Almost every workflow needs settings and runtime facts like the branch or commit, and env is how scripts receive them.
The env key defines environment variables at three scopes: workflow, job and step. The narrowest scope wins. Inside run: scripts they are ordinary shell variables ($NAME in bash).
GitHub also sets default variables on every runner, including:
GITHUB_SHA: the commit being built.GITHUB_REFandGITHUB_REF_NAME: e.g.refs/heads/mainandmain.GITHUB_REPOSITORY,GITHUB_RUN_ID,GITHUB_WORKSPACE(the checkout directory),RUNNER_OS.CI=true, which many tools use to switch to non-interactive mode.
There are two ways to refer to a value. $GITHUB_SHA is expanded by the shell on the runner. ${{ github.sha }} is an expression that GitHub substitutes into the YAML before the step runs; expressions are needed where no shell is involved, such as in with: inputs or if: conditions.
For settings that differ per repository or environment but are not secret (a region, an app name), use configuration variables defined under Settings, Secrets and variables, Actions, Variables. Read them as ${{ vars.NAME }}. They can be set at organization, repository or environment level. Anything sensitive belongs in secrets instead.
Example
env: # whole workflow
APP: shop
jobs:
build:
runs-on: ubuntu-latest
env:
STAGE: test # this job
steps:
- run: echo "$APP $STAGE on $GITHUB_REF_NAME at ${GITHUB_SHA::7}"
# shop test on main at 1a2b3c4
- run: echo "$STAGE" # prod (step scope wins)
env:
STAGE: prod
- uses: actions/setup-node@v7
with:
node-version: ${{ vars.NODE_VERSION }} # repo configuration variableRemember
- env at workflow, job or step scope; the narrowest wins
- $VAR is expanded by the shell, ${{ }} by GitHub before the step runs
- vars.* holds non-secret settings; secrets.* holds sensitive ones
Watch out for
- Using $VAR inside with: inputs: no shell runs there, so it stays a literal string.