GitHub Actions doesn’t actually store your secrets; it just provides a secure way to inject them into your workflow runs.

Let’s say you have a Python script that needs to deploy to a cloud provider. Your script needs API keys, and you absolutely don’t want those in your main.py or deploy.yml. This is where GitHub Actions secrets and environment variables come in.

Here’s a workflow snippet that uses a secret to authenticate with AWS:

name: Deploy to AWS

on: [push]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:

        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}


        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}


        aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} # Optional

        aws-region: us-east-1
    - name: Run Deployment Script
      run: python deploy.py
      env:

        MY_API_KEY: ${{ secrets.MY_CUSTOM_API_KEY }}

Notice the two ways secrets are used:

  1. secrets.AWS_ACCESS_KEY_ID: Passed directly as an argument to an action (aws-actions/configure-aws-credentials).
  2. secrets.MY_CUSTOM_API_KEY: Set as an environment variable (env) within a specific run step.

When you define AWS_ACCESS_KEY_ID and MY_CUSTOM_API_KEY in your GitHub repository’s settings under "Secrets and variables" -> "Actions," GitHub treats them as sensitive. They are encrypted at rest and only decrypted when a workflow run needs them. They are then injected into the runner environment.

Here’s how you’d set them up: Navigate to your GitHub repository. Click on "Settings". In the left sidebar, select "Secrets and variables" -> "Actions". Click the "New repository secret" button. Enter the Name (e.g., AWS_ACCESS_KEY_ID) and the Secret value (your actual AWS access key). Click "Add secret".

You can also define secrets at the organization level, which can then be used by any repository within that organization.

Environment variables, on the other hand, are for non-sensitive configuration. You can set them directly in your workflow file, or they can be "inherited" from the runner’s environment. For instance, GITHUB_SHA (the commit SHA) and GITHUB_TOKEN (a temporary token for interacting with the GitHub API) are automatically available as environment variables in every workflow run.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Print commit SHA

      run: echo "This commit is ${{ github.sha }}"

    - name: Print GITHUB_TOKEN

      run: echo "The GITHUB_TOKEN is ${{ env.GITHUB_TOKEN }}" # Accessing via env context

You can also define your own environment variables within a job or a step:

jobs:
  configure:
    runs-on: ubuntu-latest
    env:
      MY_BUILD_CONFIG: "debug" # Job-level environment variable
    steps:
    - name: Use environment variable
      run: echo "Build configuration is $MY_BUILD_CONFIG"
    - name: Use step-specific environment variable
      run: echo "Another variable"
      env:
        STEP_VAR: "specific_value"

The key distinction is sensitivity. Secrets are encrypted and masked in logs. Environment variables are not. If you put a password in an env variable without using secrets., it will be visible in your workflow logs.

The env context in your workflow allows you to access environment variables that are available to the runner. When you use secrets.YOUR_SECRET_NAME, GitHub injects that secret as an environment variable named YOUR_SECRET_NAME into the runner’s environment for that specific step or job. So, run: echo $MY_API_KEY works because MY_API_KEY was set via env: MY_API_KEY: ${{ secrets.MY_CUSTOM_API_KEY }}.

The github context provides a rich set of information about the workflow run itself, like github.repository, github.actor, github.event_name, and as seen, github.sha. You access these using github.variable_name.

When you use secrets.MY_SECRET, GitHub makes that value available as an environment variable named MY_SECRET to the runner process for the step. This is why you can access it using $MY_SECRET (in shell scripts) or ${{ env.MY_SECRET }} (within the YAML itself, though less common for direct use in run commands).

The most surprising thing about GitHub Actions secrets is that they are not truly "environment variables" in the traditional OS sense until they are injected into the runner environment. They live in GitHub’s encrypted storage and are only exposed to the runner process during a job execution, and critically, only for the specific jobs and steps that reference them. This means a secret defined in repo-a cannot be accessed by a workflow in repo-b unless explicitly shared via an organization secret or a GitHub App.

The next thing you’ll likely encounter is managing secrets for different environments like staging and production, which often involves using repository or organization-level secrets with distinct names or leveraging environment protection rules.

Want structured learning?

Take the full Github course →