10. Escribir tu propia acción
Secciones
Ajustes de lectura
Voz natural grabada para este curso, la misma en todos los navegadores.
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Tres tipos de acciones
Sección titulada «Tres tipos de acciones»La lección 6 escribió una acción compuesta: steps de YAML. Los otros dos tipos ejecutan código:
| Compuesta | JavaScript | Contenedor Docker | |
|---|---|---|---|
runs.using |
composite |
node24 (o node20) |
docker |
| Ejecuta | steps en el job del llamador | node en el runner |
un contenedor construido o descargado en el runner |
| Sistemas operativos | todos | todos | «solo pueden ejecutarse en runners con un sistema operativo Linux» |
| Inputs | ${{ inputs.x }} |
variables de entorno INPUT_X |
variables INPUT_X y args |
| Coste de arranque | ninguno | ninguno | construir o descargar la imagen |
| Analogía con C# | un método hecho de llamadas a otros métodos | una pequeña aplicación de consola | una aplicación de consola que trae su propio sistema operativo |
| Azure Pipelines | plantilla de step | tarea personalizada (Node) | job de contenedor |
Los dos ejemplos siguientes viven en este repositorio y los prueba .github/workflows/gha-10-action.yml.
Una acción JavaScript sin dependencias
Sección titulada «Una acción JavaScript sin dependencias»La misma función de slug que el código .NET y Java de la lección 2, como acción. .github/actions/slugify/action.yml:
# GitHub Actions course, lesson 10: a JavaScript action without dependenciesname: Slugifydescription: Turns a title into a URL slug, like the sample code of lesson 2.
inputs: text: description: The text to turn into a slug required: true max-length: description: Maximum length of the slug, 0 for no limit default: '0'
outputs: slug: description: The slug
runs: using: node24 main: index.js// GitHub Actions course, lesson 10: inputs arrive as INPUT_<NAME> variables, outputs go to the GITHUB_OUTPUT fileimport fs from "node:fs";
function input(name, { required = false } = {}) { const variable = `INPUT_${name.replace(/ /g, "_").toUpperCase()}`; const value = (process.env[variable] ?? "").trim(); if (required && value === "") { throw new Error(`Input required and not supplied: ${name}`); } return value;}
function slugify(text) { return text .normalize("NFD") .replace(/\p{Mn}/gu, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "");}
try { const text = input("text", { required: true }); const maxLength = Number(input("max-length") || "0"); let slug = slugify(text); if (maxLength > 0) { slug = slug.slice(0, maxLength).replace(/-+$/, ""); } console.log(`slug of "${text}" is "${slug}"`); fs.appendFileSync(process.env.GITHUB_OUTPUT, `slug=${slug}\n`);} catch (error) { console.log(`::error::${error.message}`); process.exitCode = 1;}- El runner pasa cada input como variable de entorno: «convierte los nombres de los inputs a letras mayúsculas y reemplaza los espacios por caracteres
_» (Runner.Worker/Handlers/Handler.cs). Los guiones se quedan:max-lengthpasa a serINPUT_MAX-LENGTH, un nombre que bash no puede leer como$INPUT_MAX-LENGTH, pero Node sí. - Las salidas usan el mismo archivo
$GITHUB_OUTPUTque los stepsrun:(lección 4); los errores, el mismo comando de workflow::error::. - La mayoría de las acciones reales usan para esto el paquete
@actions/core(core.getInput,core.setOutput,core.setFailed), y entonces deben incluir en el commitnode_moduleso un bundledist/index.js: el runner no ejecutanpm install. Sin dependencias, no hay nada que empaquetar.
Como la acción es solo un script, pruébala en local antes de cualquier push, con las variables que definiría el runner:
$env:INPUT_TEXT = "C# 14 & .NET 10"; $env:GITHUB_OUTPUT = "out.txt"; node .github/actions/slugify/index.jsINPUT_TEXT="C# 14 & .NET 10" GITHUB_OUTPUT=out.txt node .github/actions/slugify/index.jsINPUT_TEXT="C# 14 & .NET 10" GITHUB_OUTPUT=out.txt node .github/actions/slugify/index.jsslug of "C# 14 & .NET 10" is "c-14-net-10"Los mismos tres casos que las pruebas de .NET y Java dieron hello-world, github-actions y c-14-net-10; un texto vacío dio ::error::Input required and not supplied: text y el código de salida 1.
De .github/workflows/gha-10-action.yml, líneas 23-29:
- id: slug uses: ./.github/actions/slugify with: text: GitHub Actions course, lesson 10 — Wörld! max-length: 30 - shell: bash run: echo "slug output = ${{ steps.slug.outputs.slug }}"En los tres sistemas operativos:
javascript (ubuntu-latest) | slug of "GitHub Actions course, lesson 10 — Wörld!" is "github-actions-course-lesson-1"javascript (windows-latest) | slug of "GitHub Actions course, lesson 10 — Wörld!" is "github-actions-course-lesson-1"javascript (macos-latest) | slug of "GitHub Actions course, lesson 10 — Wörld!" is "github-actions-course-lesson-1"javascript (ubuntu-latest) | slug output = github-actions-course-lesson-1El mismo resultado en todas partes, y el step de la acción tardó menos de un segundo. Fíjate en el 1: max-length: 30 partió 10 por la mitad. El código hace exactamente lo que dice; si un slug debe cortarse al final de una palabra es una pregunta de especificación que la prueba no hizo.
Una acción de contenedor
Sección titulada «Una acción de contenedor».github/actions/hello-container tiene tres archivos:
.github/actions/hello-container/action.yml:
# GitHub Actions course, lesson 10: a container action (Linux runners only)name: Hello from a containerdescription: Prints the operating system of the container it runs in.
inputs: who: description: Who to greet default: world
runs: using: docker image: Dockerfile args: - ${{ inputs.who }}.github/actions/hello-container/Dockerfile:
# GitHub Actions course, lesson 10: the image of the container action, built on the runner at every runFROM alpine:3.22COPY entrypoint.sh /entrypoint.shENTRYPOINT ["/entrypoint.sh"].github/actions/hello-container/entrypoint.sh:
#!/bin/sh# GitHub Actions course, lesson 10: runs inside the container; the workspace is mounted at /github/workspaceset -eecho "hello $1 from $(. /etc/os-release && echo "$PRETTY_NAME")"echo "workdir: $(pwd)"echo "INPUT_WHO=$INPUT_WHO"echo "greeting=hello $1" >> "$GITHUB_OUTPUT"Primera ejecución: permiso denegado
Sección titulada «Primera ejecución: permiso denegado»##[command]/usr/bin/docker build -t 128b89:7c5dc9951c794fae826f3098f2786173 -f "/home/runner/work/learn/learn/./.github/actions/hello-container/Dockerfile" "/home/runner/work/learn/learn/.github/actions/hello-container"...docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "/entrypoint.sh": permission deniedEl archivo se escribió en Windows, donde Git no registra el bit de ejecución (git config core.filemode → false), así que se incluyó en el commit como 100644 — se dejó así a propósito, para capturar este error, con el que todo autor de una acción de contenedor en Windows se encuentra una vez. La corrección está en Git, no en el Dockerfile:
git update-index --chmod=+x .github/actions/hello-container/entrypoint.shgit diff --cached --summary mode change 100644 => 100755 .github/actions/hello-container/entrypoint.shSegunda ejecución
Sección titulada «Segunda ejecución»hello Grace from Alpine Linux v3.22workdir: /github/workspaceINPUT_WHO=Gracegreeting output = hello GraceEl comando docker run del runner muestra cómo ve el contenedor el job:
-v "/var/run/docker.sock":"/var/run/docker.sock"-v "/home/runner/work/_temp":"/github/runner_temp"-v "/home/runner/work/_temp/_github_home":"/github/home"-v "/home/runner/work/_temp/_github_workflow":"/github/workflow"-v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands"-v "/home/runner/work/learn/learn":"/github/workspace"El workspace está montado, así que el contenedor puede leer el código obtenido con el checkout, y $GITHUB_OUTPUT apunta a la carpeta montada file_commands, así que las salidas funcionan. El step tardó 5 segundos, casi todo en construir la imagen — frente a menos de un segundo para la acción JavaScript. Una acción de contenedor publicada suele apuntar image: a una imagen ya construida (docker://ghcr.io/…) para evitar la construcción.
Publicar, en resumen
Sección titulada «Publicar, en resumen»Una acción en su propio repositorio público, con action.yml en la raíz, se usa como owner/repo@v1. Las convenciones, según Gestionar acciones personalizadas: publicar versiones con tags de versionado semántico (v1.2.0) y mover un tag mayor (v1) a la última versión compatible — el tag móvil del que la lección 7 aconseja a los consumidores no fiarse a ciegas. Listarla en el Marketplace es opcional. No se hace en este curso: las acciones de aquí se quedan en local.
Puntos clave
Sección titulada «Puntos clave»- Las acciones JavaScript se ejecutan en todos los runners y arrancan al instante; las acciones de contenedor traen su propio Linux y solo se ejecutan en runners Linux.
- Los inputs llegan como variables
INPUT_<NAME>(en mayúsculas, con los guiones); las salidas y los errores usan$GITHUB_OUTPUTy los comandos de workflow, como cualquier step. - El runner no impone
required: true: la acción tiene que comprobarlo. - Incluye en el commit lo que la acción necesita para ejecutarse: nada de
npm installen el runner, bits de ejecución para los scripts, y cuidado con elpackage.jsondel repositorio.
Ejercicios
Sección titulada «Ejercicios»- Un workflow pasa
txt: Helloen lugar detext: Helloa la acción slugify. ¿Qué hace el runner con el input desconocido, y con el obligatorio que falta?
Solución
Según gha-10-exercises.yml:
##[warning]Unexpected input(s) 'txt', valid inputs are ['text', 'max-length']##[error]Input required and not supplied: textEl input desconocido solo produce una advertencia, y el step se ejecutó de todos modos. El error viene del propio código de la acción: «Las acciones que usan required: true no devolverán automáticamente un error si el input no se especifica». Sin la comprobación en index.js, la acción habría producido un slug vacío y un step en verde.
- ¿Qué pasa cuando se usa
hello-containeren un jobwindows-latest?
Solución
##[error]Container action is only supported on LinuxEl step falla inmediatamente, antes de construir nada. Una acción pensada para los tres sistemas operativos tiene que ser JavaScript o compuesta.
- La primera versión de
index.jsempezaba conconst fs = require("node:fs");, Node.js válido. ¿Se ejecutaría en el runner?
Solución
ReferenceError: require is not defined in ES module scope, you can use import insteadThis file is being treated as an ES module because it has a '.js' file extension and '/home/runner/work/learn/learn/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.Node busca el package.json más cercano por encima del script, y en este repositorio es el del sitio Astro, que declara "type": "module". No. El error de arriba viene del runner, al ejecutar la copia exercise-commonjs; el mismo error había aparecido antes en local. Soluciones: la sintaxis import (la elegida aquí), un archivo index.cjs, o un package.json en la carpeta de la acción. En un repositorio dedicado a la acción, ese archivo lo controlas tú.