Contexts и expressions
Справочник по `${{ }}`: contexts `github`, `env`, `vars`, `secrets`, `matrix`, `needs`, `inputs`, операторы, функции, `fromJSON`, статусные функции и различие между expression-time и shell runtime.
Contexts и expressions
Expression в GitHub Actions — это небольшой язык вычислений внутри YAML. Всё, что написано в форме ${{ ... }}, обрабатывается движком GitHub Actions: он подставляет значения из contexts, сравнивает строки, вызывает функции, решает if, строит matrix, формирует runs-on, env, with и другие поля workflow. Это не Bash, не PowerShell и не JavaScript, хотя синтаксис местами похож.
Главная практическая граница такая: expression-time происходит на стороне GitHub Actions при подготовке workflow/job/step, а shell runtime происходит уже на runner, когда выполняется run. В Jobs, dependencies и conditions это проявлялось в if: GitHub может решить не отправлять job на runner вообще. Внутри run уже работает shell, видит $GITHUB_REF, $NODE_VERSION, $MY_VAR и не понимает ${{ }} — к этому моменту выражения уже подставлены как текст.
flowchart TD
A[Workflow YAML] --> B[GitHub Actions engine]
B --> C{Expressions ${{ ... }} }
C --> D[Conditions: if, needs, strategy, runs-on]
C --> E[Text substituted into run blocks]
D --> F[Job selected and sent to runner]
E --> F
F --> G[Shell runtime: bash, pwsh, cmd]
G --> H[$GITHUB_ENV and $GITHUB_OUTPUT]
H --> I[Later steps/jobs read env, steps outputs, needs outputs]Expression-time против shell runtime
Один и тот же факт часто доступен двумя способами: через context и через environment variable. Например, ветку можно прочитать как ${{ github.ref }} во время обработки workflow или как $GITHUB_REF внутри shell-скрипта на runner.
name: Expression demo
on: push
jobs:
inspect:
if: ${{ startsWith(github.ref, 'refs/heads/') }}
runs-on: ubuntu-latest
steps:
- name: Compare GitHub expression and shell variable
run: |
echo "Expression value: ${{ github.ref }}"
echo "Shell value: $GITHUB_REF"Первый echo опасно читать как «Bash сам знает github.ref». Нет: GitHub заранее заменит ${{ github.ref }} на строку, а потом отдаст готовый скрипт runner. Поэтому для внешних данных из event payload — названия PR, body issue, имени branch от форка — безопаснее передавать значение через env, а в shell обращаться к переменной в кавычках.
steps:
- name: Print PR title safely
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
printf 'PR title: %s\n' "$PR_TITLE"Это не делает данные «доверенными», но убирает класс ошибок, где непредсказуемый текст прямо вшивается в shell-команду. Подробная security-модель рядом с этим живёт в Secure use и threat model и pull_request_target и untrusted PR.
Основные contexts
Context — это объект данных workflow. К нему обращаются через точку (github.sha) или индекс (github['sha']). Если свойства нет, результат обычно становится пустой строкой, что удобно для простых проверок, но опасно для опечаток: ${{ github.ref_namee }} не упадёт как строгий язык.
Чаще всего в обычном CI/CD встречаются такие contexts:
github— сведения о run, event, ref, sha, actor, repository и payload события. Это центральный context для условий по branch, tag, event action и PR.env— переменные, заданные в YAML на уровне workflow, job или step. Это не то же самое, что все environment variables runner.vars— configuration variables, заданные на уровне organization, repository или environment. Их удобно использовать для не секретных настроек: имя registry, регион cloud, флаг включения feature.secrets— секреты, доступные текущему run. Если secret не задан, обращение вроде${{ secrets.DEPLOY_TOKEN }}даёт пустую строку.matrix— текущая комбинация matrix job: например,matrix.nodeилиmatrix.os. Полная тема — Matrix strategy.needs— статусы и outputs jobs, от которых текущий job зависит черезneeds. Это мост между графом jobs и выражениями.inputs— параметрыworkflow_dispatchилиworkflow_call. Особенно важен для ручных запусков и Reusable workflows.steps— outputs и outcome/conclusion уже выполненных steps в текущем job.runner— сведения о текущем runner: OS, architecture, temp paths и окружение выполнения.
Не все contexts доступны везде. Например, jobs.<job_id>.if вычисляется до отправки job на runner, поэтому там нет runner и steps. steps.if знает больше: уже есть runner, предыдущие steps и hashFiles. secrets нельзя напрямую использовать в if; типичный обход — переложить secret в job-level env и проверять env.<name>, если это действительно нужно.
jobs:
publish:
runs-on: ubuntu-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Publish only when token exists
if: ${{ env.NPM_TOKEN != '' }}
run: npm publishОператоры и типы
Expressions поддерживают группировку ( ), доступ к полям ., индекс [ ], отрицание !, сравнения < <= > >= == !=, логические && и ||. Строки внутри ${{ }} пишутся в одинарных кавычках: ${{ 'production' }}. Двойные кавычки внутри expression для string literal использовать нельзя.
Сравнения в Actions не строгие. Если типы отличаются, GitHub делает loose coercion: null превращается в 0, true — в 1, пустая строка — в 0, нечисловая строка — в NaN. Строковые сравнения регистронезависимы. На практике это значит: для важных числовых сравнений не полагайтесь на «само как-нибудь сравнит», а приводите строку через fromJSON().
env:
TIMEOUT_MINUTES: '15'
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Run e2e with numeric timeout
timeout-minutes: ${{ fromJSON(env.TIMEOUT_MINUTES) }}
run: npm run test:e2eДля ветвления сейчас удобно использовать case(): оно читабельнее, чем трюк через && и ||, когда нужно выбрать строку по нескольким условиям.
env:
DEPLOY_ENV: ${{ case(
github.ref == 'refs/heads/main', 'production',
github.ref == 'refs/heads/staging', 'staging',
'preview'
) }}Функции: contains, fromJSON, toJSON, hashFiles
contains(), startsWith() и endsWith() закрывают большинство условий по branch, tag, label и event name. Для набора допустимых событий часто чище написать массив через fromJSON, чем цепочку ||.
if: ${{ contains(fromJSON('["push", "pull_request"]'), github.event_name) }}toJSON() полезен для диагностики contexts, но не печатайте весь github или secrets context бездумно. GitHub маскирует секреты в логах, но это не лицензия на dumping всего состояния: некоторые поля чувствительны, а преобразованные значения могут маскироваться не так, как вы ожидаете. Для диагностики лучше выводить узкие фрагменты.
steps:
- name: Debug event shape
env:
EVENT_ACTION: ${{ github.event.action }}
REF_NAME: ${{ github.ref_name }}
run: |
printf 'action=%s ref=%s\n' "$EVENT_ACTION" "$REF_NAME"hashFiles() считает SHA-256 по файлам внутри GITHUB_WORKSPACE; его часто используют в key для Dependency cache. Если glob ничего не нашёл, функция возвращает пустую строку.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}Outputs, needs и динамическая matrix
Expression становится особенно полезным, когда один job принимает решение, а другой использует его. Маленькие значения передают через step outputs и job outputs; файлы и отчёты — через Artifacts и reports.
jobs:
plan:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.pick.outputs.matrix }}
steps:
- id: pick
run: |
echo 'matrix={"include":[{"node":20,"os":"ubuntu-latest"},{"node":22,"os":"ubuntu-latest"}]}' >> "$GITHUB_OUTPUT"
test:
needs: plan
runs-on: ${{ matrix.os }}
strategy:
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm testЗдесь plan пишет JSON-строку в $GITHUB_OUTPUT; test читает её как needs.plan.outputs.matrix; fromJSON() превращает строку в настоящий object для strategy.matrix. Это хороший пример границы между runtime и expression-time: shell создал строку, GitHub использовал её для построения следующих jobs.
Status functions
В if по умолчанию действует success(), если вы явно не указали status function. Поэтому «запусти диагностику после падения» почти всегда должно включать failure().
steps:
- name: Build
id: build
run: npm run build
- name: Upload logs after failure
if: ${{ failure() && steps.build.conclusion == 'failure' }}
uses: actions/upload-artifact@v4
with:
name: build-logs
path: logs/success() означает, что предыдущие steps успешны. failure() срабатывает после failure, в цепочке зависимых jobs учитывает и предков. cancelled() проверяет отмену workflow. always() запускает step даже после failure/cancel, но для критичных операций вроде checkout лучше не использовать его без причины; часто безопаснее if: ${{ !cancelled() }}. Диагностика skipped/pending статусов подробнее относится к Monitoring и troubleshooting.
See also
- Workflow: файл, запуск, jobs и steps — где expressions появляются в структуре workflow.
- Events и filters — какие данные попадают в
github.eventи чем trigger filters отличаются отif. - Jobs, dependencies и conditions — граф jobs,
needs, status functions и условия выполнения. - Variables, env и secrets — различия между
env, default variables,varsиsecrets. - Step outputs и job outputs — как
$GITHUB_OUTPUTстановитсяsteps.*.outputsиneeds.*.outputs. - Permissions и GITHUB_TOKEN — почему contexts с токенами и секретами требуют минимальных прав.
- Matrix strategy — как expressions разворачивают варианты job.
- Dependency cache — практическое применение
hashFiles()в cache keys.
