Variables, env и secrets
Различия между `env`, repository/org/environment variables, secrets и contexts; области видимости, наследование, маскирование в логах и практики именования.
Variables, env и secrets
В GitHub Actions есть несколько похожих, но не взаимозаменяемых способов хранить значения: env, configuration variables (vars), secrets и contexts. Все они выглядят как «переменные», но отвечают за разные задачи. Ошибка обычно начинается там, где одно и то же значение сначала кладут в repository secret, потом дублируют в env, потом пытаются прочитать его через ${{ github.* }} и удивляются, почему оно видно не там, где ожидалось.
Короткая практическая граница такая: env — это переменные окружения для команд и actions во время выполнения job; vars — несекретная конфигурация, заданная в настройках repository, organization или environment; secrets — чувствительные значения, которые GitHub маскирует в логах и не показывает обратно после сохранения; contexts — объекты данных, через которые expression engine подставляет значения в YAML до или во время выполнения отдельных частей workflow. Эта статья продолжает модель из Workflow: файл, запуск, jobs и steps, Contexts и expressions и Permissions и GITHUB_TOKEN: теперь важен не только синтаксис, но и место, где значение появляется.
flowchart TD
A[Где хранится значение?] --> B[env в workflow/job/step]
A --> C[vars в organization/repository/environment]
A --> D[secrets в organization/repository/environment]
A --> E[contexts: github, matrix, needs, steps]
B --> B1[Runtime-переменные для shell и actions]
C --> C1[Несекретная конфигурация через ${{ vars.NAME }}]
D --> D1[Чувствительные значения через ${{ secrets.NAME }}]
E --> E1[Данные expression engine для if, with, env, concurrency]
B1 --> F[Step получает environment variables]
C1 --> F
D1 --> F
E1 --> G[YAML-решения до/во время выполнения job]
F --> H[run: npm test / deploy / publish]
G --> Henv: значение для процесса
env задаёт environment variables, которые увидит shell-команда или action. Его можно объявить на уровне workflow, job или step. Чем ближе уровень к step, тем уже область видимости и тем выше приоритет при совпадении имени.
name: CI
on: [push]
env:
NODE_ENV: test
APP_NAME: billing-api
jobs:
test:
runs-on: ubuntu-latest
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
steps:
- uses: actions/checkout@v4
- name: Print runtime env
run: |
echo "APP_NAME=$APP_NAME"
echo "NODE_ENV=$NODE_ENV"
- name: Override only this step
env:
NODE_ENV: production-like
run: echo "NODE_ENV=$NODE_ENV"Внутри run удобнее читать $NODE_ENV, $DATABASE_URL, $APP_NAME, потому что это уже обычные переменные окружения runner. А вот в YAML-полях, которые обрабатывает GitHub expression engine, обычно используют ${{ env.NODE_ENV }}. Это две разные стадии чтения: expression ${{ ... }} подставляется GitHub Actions, а $NAME раскрывает shell на runner.
steps:
- name: Use env in expression and shell
if: ${{ env.NODE_ENV == 'test' }}
run: echo "Shell sees NODE_ENV=$NODE_ENV"Хорошее правило: env подходит для runtime-настроек конкретного workflow: режим тестов, путь к build-директории, флаги CLI, временные значения, которые не жалко показать в логах. Для передачи вычисленного значения из одного step в следующий чаще лучше использовать environment files, например GITHUB_ENV, а для структурированной передачи результата — Step outputs и job outputs.
vars: конфигурация без секрета
Configuration variables задаются не прямо в YAML, а в Settings репозитория, организации или environment. В workflow они читаются через vars context:
name: Deploy preview
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
environment: preview
env:
PUBLIC_API_BASE_URL: ${{ vars.PUBLIC_API_BASE_URL }}
FEATURE_FLAGS: ${{ vars.FEATURE_FLAGS }}
steps:
- run: |
echo "Deploying to $PUBLIC_API_BASE_URL"
echo "Flags: $FEATURE_FLAGS"vars хорошо подходят для значений, которые являются конфигурацией, но не секретом: публичный base URL, имя cloud region, имя package registry, feature flag, project id, имя S3 bucket, если само имя не раскрывает ничего опасного. Если значение можно спокойно написать в README, его обычно можно хранить в vars, а не в secrets.
У vars есть важный плюс для governance: одну и ту же переменную можно задать на уровне organization, переопределить в repository и отдельно настроить для environment вроде staging или production. Это особенно удобно вместе с Environments и protection rules: один workflow может использовать одинаковый YAML, но получать разные URL, регионы или имена ресурсов в зависимости от выбранного environment.
secrets: чувствительные значения
Secrets нужны для значений, которые нельзя раскрывать в логах и PR: API tokens, webhook secrets, private keys, passwords, signing keys. В workflow они доступны через secrets context:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Publish package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}GitHub маскирует secrets в логах, но маскирование не надо считать криптографической защитой от любого вывода. Если секрет преобразовать, склеить с другими строками, закодировать, распечатать частями или положить в structured JSON, GitHub может не распознать все варианты. Поэтому в Secure use и threat model secrets рассматриваются как данные, которые надо не только маскировать, но и не передавать лишнему коду.
Есть ещё один практический момент: secrets недоступны в workflow runs из forks, кроме GITHUB_TOKEN с ограничениями, и не передаются в Dependabot events так же, как в обычные trusted runs. Это сделано специально: untrusted PR не должен получить production token. Если workflow должен проверять внешние PR, держите test job без secrets, а privileged job отделяйте условиями, approvals или environment protection rules. Для опасных сценариев рядом стоят pull_request_target и untrusted PR и Runner governance.
Для cloud deploy современная норма — не хранить long-lived cloud access key в secrets, если provider поддерживает OIDC. Тогда workflow получает короткоживущий token через id-token: write, а доверие настраивается на стороне cloud provider. Это тема Cloud deploy через OIDC и OIDC и секреты облаков, но граница уже видна здесь: GitHub secret — запасной вариант для долгоживущей тайны, OIDC — предпочтительный путь для облачных credentials.
Contexts: не все значения живут в shell
Contexts — это объекты, из которых GitHub Actions берёт данные для expressions: github, env, vars, secrets, job, steps, runner, matrix, needs, inputs и другие. Они особенно важны в if, with, env, name, run-name, concurrency и других YAML-полях, которые поддерживают expressions.
jobs:
test:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/main' }}
env:
CI_BRANCH: ${{ github.ref_name }}
CACHE_PREFIX: ${{ vars.CACHE_PREFIX }}
steps:
- run: echo "Running on $CI_BRANCH"Главная ловушка: context expression и shell variable не одно и то же. В if нельзя написать if: $NODE_ENV == 'test'; GitHub не запускает shell, чтобы решить, выполнять ли step. Надо писать ${{ env.NODE_ENV == 'test' }}. А внутри run наоборот естественнее использовать $NODE_ENV, потому что команда уже выполняется на runner.
# Неверно: это поле читает GitHub Actions, а не shell.
if: $NODE_ENV == 'test'
# Верно: expression engine читает env context.
if: ${{ env.NODE_ENV == 'test' }}Ещё одна ловушка — доступность contexts зависит от места в YAML. Например, secrets нельзя свободно использовать в каждом поле workflow, а steps.<id>.outputs появляется только после выполнения step с этим id. Когда значение нужно передать между jobs, не пытайтесь «дотянуться» до environment variable другого job: jobs изолированы. Используйте Step outputs и job outputs, Artifacts и reports или Dependency cache — в зависимости от задачи.
Наследование и приоритеты
Для env ближайший scope выигрывает: step переопределяет job, job переопределяет workflow. Это удобно, но может прятать ошибки, если везде использовать одинаковые имена вроде TOKEN, URL, ENV. Лучше писать имена с доменом смысла: NODE_AUTH_TOKEN, PUBLIC_API_BASE_URL, AWS_REGION, DEPLOY_ENV, TEST_DATABASE_URL.
env:
DEPLOY_ENV: staging
jobs:
deploy:
runs-on: ubuntu-latest
env:
DEPLOY_ENV: production
steps:
- run: echo "$DEPLOY_ENV" # productionДля vars и secrets приоритет зависит от уровня, где они заданы: organization, repository, environment. Environment-level значения применяются только к job, который объявил environment. Это не просто namespace; для environment secrets GitHub сначала применяет protection rules. Иными словами, job не должен получить production secret до approval, если environment требует approval.
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy with production secret
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Этот фрагмент выглядит коротко, но в нём спрятана важная эксплуатационная идея: YAML не обязан знать сам production token. Он только говорит: «для job нужен environment production». А уже environment решает, какие vars/secrets доступны и какие protection rules надо пройти.
Практики именования и хранения
Не кладите секреты в env на верхнем уровне workflow. Технически можно написать env: TOKEN: ${{ secrets.X }}, но тогда secret становится доступен всем jobs и steps этого workflow, включая сторонние actions. Лучше передавать secret ровно в тот step, который его использует.
# Плохо: secret попадает во все jobs и steps этого workflow.
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
# Лучше: secret доступен только конкретному step.
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Publish metadata
run: ./scripts/publish-metadata.sh
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}Не используйте secrets как склад любой конфигурации. Когда всё лежит в secrets, workflow становится менее читаемым: непонятно, какие значения реально чувствительные, а какие просто настройки окружения. Публичный URL в secret не становится безопаснее; он просто становится менее видимым для ревью.
Не печатайте contexts целиком. toJSON(github) полезен для debug, но contexts могут содержать чувствительные или шумные поля. Для troubleshooting лучше выводить конкретные безопасные значения и оформлять полезный результат через Workflow commands и summaries.
Удобный naming baseline:
PUBLIC_* несекретные значения, которые могут попасть в клиент или логи
*_TOKEN чувствительный токен, обычно secret
*_URL URL; secret только если сам URL содержит credential
*_REGION vars или env
*_ENV имя окружения: staging, production, preview
TEST_* значения только для CI/test jobМини-пример: один workflow, три типа данных
Ниже workflow показывает нормальное разделение: vars задаёт публичную конфигурацию, env — runtime-флаги, secrets — токен только для publish step.
name: Build and publish
on:
push:
branches: [main]
permissions:
contents: read
packages: write
env:
NODE_ENV: production
DIST_DIR: dist
jobs:
build:
runs-on: ubuntu-latest
env:
PUBLIC_API_BASE_URL: ${{ vars.PUBLIC_API_BASE_URL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
env:
VITE_API_BASE_URL: ${{ env.PUBLIC_API_BASE_URL }}
- run: tar -czf app.tgz "$DIST_DIR"
publish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Publish artifact metadata
run: ./scripts/publish-metadata.sh
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}У этого примера есть намеренное упрощение: файл app.tgz не попадёт из build в publish, потому что jobs изолированы. Для настоящего handoff нужен artifact из Artifacts и reports. Зато разделение данных здесь правильное: несекретная конфигурация идёт через vars, runtime-настройки — через env, чувствительный token — только в один step.
See also
- Contexts и expressions — где и как вычисляются
${{ ... }}. - Permissions и GITHUB_TOKEN — почему token и secrets надо сочетать с минимальными permissions.
- Step outputs и job outputs — как передавать вычисленные значения между steps и jobs.
- Artifacts и reports — как передавать файлы и отчёты между jobs.
- Workflow commands и summaries — как писать в
GITHUB_ENV,GITHUB_OUTPUTи job summary. - Environments и protection rules — как environment vars/secrets связываются с approvals и deploy-контролем.
- Secure use и threat model — модель угроз для secrets, third-party actions и untrusted input.
- OIDC и секреты облаков — когда cloud secrets лучше заменить короткоживущими credentials.
