GitHub Actions: CI/CD Workflows From First Run to Secure Deployments · GitHub Actions Workflows: Triggers, CI and Matrices

Variables, Secrets, Expressions and Outputs

Configure workflows with env, vars and secrets, use ${{ }} expressions and if conditions, and pass values between steps.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A workflow that uses a repository variable, a secret, a conditional step, and a value passed from one step to another through GITHUB_OUTPUT.

The idea

  • env: sets environment variables at the workflow, job or step level.
  • vars.NAME reads a configuration variable (plain text, visible) set in repository or organization settings.
  • secrets.NAME reads an encrypted secret. Its value is masked in logs, and it is not available to workflows from forks.
  • Expressions ${{ ... }} read contexts (github, env, vars, secrets, matrix, needs, steps, runner) and call functions like contains(), startsWith(), format() and hashFiles().
  • if: decides whether a step or job runs. Use status functions success(), failure() and always().
  • Step outputs: write name=value to the file $GITHUB_OUTPUT, give the step an id, then read ${{ steps.<id>.outputs.name }} later.

The code

env, vars, secrets, conditions and step outputs
name: Config demo
on: push

env:
  APP_NAME: shop

jobs:
  demo:
    runs-on: ubuntu-latest
    env:
      REGION: ${{ vars.REGION || 'eu-west-1' }}
    steps:
      - name: Compute a version
        id: ver
        run: echo "version=1.0.${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"

      - name: Use the output
        run: echo "Building $APP_NAME ${{ steps.ver.outputs.version }} for $REGION"

      - name: Call an API with a secret
        env:
          API_KEY: ${{ secrets.API_KEY }}
        run: >-
          curl --fail -H "Authorization: Bearer $API_KEY"
          https://api.example.com/health

      - name: Only on the main branch
        if: github.ref == 'refs/heads/main'
        run: echo "On main"

      - name: Always report
        if: ${{ always() }}
        run: echo "Job finished"

Output

actionlint: no problems found
workflow: Config demo
triggers: push
jobs, in the order they run:
  demo: runs on ubuntu-latest | 5 step(s)

Run this on your computer — see “Get Started” for setup.

The secret goes through an environment variable, never straight into the script text; that matters for security (see the last module).

Common mistakes

  • Putting secrets in vars. Variables are not masked. Anything sensitive belongs in secrets.
  • Missing secret = empty string. A mistyped secret name doesn't fail; it silently becomes empty. Check the secret exists.
  • Using $GITHUB_ENV and expecting the value in the same step. Variables written to GITHUB_ENV are available to later steps only.
  • Quoting problems in if:. Write string literals with single quotes inside expressions: github.ref == 'refs/heads/main'.

Key points

  • env, vars and secrets provide configuration; secrets are masked.
  • ${{ }} expressions read contexts; if: gates steps and jobs.
  • Share values between steps with $GITHUB_OUTPUT and step ids. Further reading: GitHub Docs.

Practice questions

How does a step pass a computed value to a later step?

Answer

It appends name=value to the file named by $GITHUB_OUTPUT, has an id, and the later step reads ${{ steps.<id>.outputs.name }}.

What happens if you reference a secret that doesn't exist?

Answer

No error: the expression evaluates to an empty string. You should verify the secret is set, or fail the step if the value is empty.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?