JavaScript actions
Структура Node-based action: `@actions/core`, `@actions/github`, packaging, runtime compatibility, outputs, error handling, logging и release artifacts.
JavaScript actions
JavaScript action — это custom action, который выполняется прямо на runner через Node.js runtime, указанный в action.yml. В отличие от Composite actions, здесь логика живёт не как набор YAML-steps, а как JavaScript-код: можно нормально обрабатывать JSON, вызывать GitHub API, валидировать inputs, строить outputs и делать аккуратный error handling. В отличие от Docker container actions, JavaScript action не тащит свой Linux-контейнер и может работать на Ubuntu, Windows и macOS runners, если код не зависит от внешних бинарников.
Практическое правило: JavaScript action нужен, когда shell уже стал хрупким. Например, action должна прочитать pull request labels, найти изменённые packages, выставить output deploy-needed и оставить понятный лог. Такой код проще сопровождать в Node.js, чем в многострочном bash с jq, кавычками и платформенными отличиями.
flowchart TD
A[Caller workflow step: uses owner/action@ref] --> B[action.yml]
B --> C{runs.using}
C -->|node20 / node24| D[Runner запускает dist/index.js]
D --> E[@actions/core: inputs, outputs, logs]
D --> F[@actions/github: context + Octokit API]
E --> G[Step outputs и annotations]
F --> H[GitHub API с правами job token]Структура repo
Минимальный JavaScript action repo обычно выглядит так:
my-js-action/
action.yml
package.json
package-lock.json
src/
index.js
dist/
index.js
README.mdaction.yml — публичный контракт action. Он описывает имя, inputs, outputs и то, какой файл запускать:
name: "Detect changed package"
description: "Find a changed workspace package and expose it as an output"
inputs:
token:
description: "GitHub token for API calls"
required: true
base-ref:
description: "Base git ref to compare against"
required: false
default: "main"
outputs:
package:
description: "Detected package name, or empty string"
changed:
description: "true when a package changed"
runs:
using: "node24"
main: "dist/index.js"На момент актуальной GitHub Docs для JavaScript actions поддерживаются node20 и node24 в runs.using. Если action уже опубликована на старом runtime, migration лучше делать как обычный breaking-risk change: проверить зависимости, собрать новый dist, выпустить tag и явно описать совместимость. Это связано с Versioning и maintenance actions, а не только с синтаксисом YAML.
@actions/core: inputs, outputs и статус
Пакет @actions/core — основной слой между вашим Node-кодом и GitHub Actions workflow commands. Через него читают inputs, пишут outputs, управляют логами и помечают action как failed.
const core = require("@actions/core");
async function run() {
try {
const baseRef = core.getInput("base-ref") || "main";
const token = core.getInput("token", { required: true });
core.info(`Comparing against ${baseRef}`);
const detectedPackage = "web";
core.setOutput("package", detectedPackage);
core.setOutput("changed", detectedPackage.length > 0 ? "true" : "false");
core.notice(`Detected package: ${detectedPackage}`);
} catch (error) {
core.setFailed(error instanceof Error ? error.message : String(error));
}
}
run();Важная деталь: required: true в metadata описывает контракт, но надёжную runtime-валидацию лучше делать в коде через core.getInput("name", { required: true }). Outputs для JavaScript action объявляются в action.yml, но значение задаёт код через core.setOutput. Это отличается от composite action, где metadata мапит output через value: ${{ steps... }}. В caller workflow результат читается обычным способом, как в Step outputs и job outputs:
steps:
- uses: actions/checkout@v6
- id: detect
uses: acme/detect-changed-package@v1
with:
token: ${{ github.token }}
base-ref: main
- run: echo "Package: ${{ steps.detect.outputs.package }}"@actions/github: context и API
Пакет @actions/github даёт два главных инструмента: github.context и authenticated Octokit client. context похож по смыслу на данные из Contexts и expressions, но доступен уже внутри Node-кода: repo, owner, event payload, ref, actor, SHA.
const core = require("@actions/core");
async function run() {
try {
const github = await import("@actions/github");
const token = core.getInput("token", { required: true });
const octokit = github.getOctokit(token);
const { owner, repo } = github.context.repo;
const pullNumber = github.context.payload.pull_request?.number;
if (!pullNumber) {
core.info("Not a pull_request event; nothing to inspect.");
core.setOutput("changed", "false");
return;
}
const { data: files } = await octokit.rest.pulls.listFiles({
owner,
repo,
pull_number: pullNumber,
per_page: 100
});
const changed = files.some((file) => file.filename.startsWith("packages/web/"));
core.setOutput("changed", changed ? "true" : "false");
} catch (error) {
core.setFailed(error instanceof Error ? error.message : String(error));
}
}
run();Права здесь задаёт не action, а caller job. Если API-запросу нужно читать PR files, создавать comments или писать checks, workflow должен выдать соответствующие Permissions и GITHUB_TOKEN. Action не должна молча предполагать broad token: лучше документировать минимальные permissions в README и падать с понятной ошибкой, если API вернул 403.
Logging: полезный шум, а не дамп payload
Для логов используйте core.info, core.debug, core.notice, core.warning, core.error, core.startGroup и core.endGroup. debug виден только при включённом debug logging, поэтому туда можно отправлять диагностические детали. warning и notice хороши для аннотаций, которые должны быть видны в UI workflow run.
Не печатайте целиком github.context.payload в production action. Payload может быть большим, шумным и содержать данные из untrusted источников: PR title, branch name, issue body, commit message. Это та же граница доверия, что в Secure use и threat model и pull_request_target и untrusted PR. Secrets GitHub старается маскировать, но action всё равно не должна превращать логи в свалку входных данных.
Если action создаёт человеческий отчёт, лучше писать короткий summary через workflow commands или отдельный файл, который caller загрузит как artifact. Для больших отчётов используйте Artifacts и reports, а не outputs.
Packaging: почему dist обычно коммитят
JavaScript action запускается из Git ref, который указал пользователь: owner/repo@v1, @v1.2.3 или SHA. Runner не обязан выполнять npm install перед запуском action. Поэтому published JavaScript action обычно содержит собранный dist/index.js вместе с зависимостями, упакованными bundler-ом. В современных шаблонах GitHub для JavaScript action это часто отдельная команда вроде npm run bundle, после которой action.yml указывает на dist/index.js.
Это непривычно для обычного Node.js package, где dist часто не коммитят. Для action repo логика другая: release artifact — это сам git tag с metadata и runnable dist. Если tag указывает на commit без собранного файла, consumer увидит падение уже в своём workflow.
runs:
using: "node24"
main: "dist/index.js"Production-friendly release flow обычно такой:
name: release
on:
release:
types: [published]
jobs:
verify:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm test
- run: npm run bundle
- run: git diff --exit-code dist/index.jsПоследняя команда защищает от типичной ошибки: source изменили, а bundled dist забыли обновить. В реальном release workflow ещё добавляют semver tags (v1, v1.2, v1.2.3) и правила публикации, но это уже отдельная тема Versioning и maintenance actions.
Когда JavaScript action не подходит
JavaScript action не должна становиться универсальным deploy-сервисом с собственной политикой доступа. Job-level решения — environment, approvals, cloud credentials, permissions, matrix — должны оставаться в workflow или Reusable workflows. Action может помочь вычислить данные и выполнить узкую операцию, но не должна прятать критическую CD-политику от ревьюера.
Если action требует системные пакеты, конкретную версию Linux library или тяжёлый CLI, лучше посмотреть на Docker container actions. Если логика — всего лишь три shell-команды с понятными inputs, Composite actions будут дешевле и прозрачнее. Если вы берёте готовую action из чужого repo, применяются те же правила выбора и pinning, что в Marketplace actions и pinning.
See also
- Composite actions — когда достаточно собрать несколько steps без Node-кода.
- Docker container actions — когда важнее контролируемое Linux-окружение.
- Reusable workflows — когда переиспользуется job или pipeline целиком.
- Permissions и GITHUB_TOKEN — какие права реально получает action при API-вызовах.
- Marketplace actions и pinning — как безопасно потреблять опубликованные actions.
- Versioning и maintenance actions — tags, releases, changelog и backward compatibility.
- Workflow commands и summaries — logging, annotations и job summary как интерфейс action.
Внешние справки: GitHub Docs по creating a JavaScript action, metadata syntax, custom actions, release and maintain actions, а также официальный repo actions/toolkit.
