Github Actions Managing Your Workflow Environments Created: 06 Apr 2026 Updated: 06 Apr 2026

Deployment Environments

Contents

  1. What Are Environments?
  2. Deployment Protection Rules
  3. Environment-Scoped Secrets and Variables
  4. Referencing an Environment in a Job
  5. The Environment URL
  6. Repository Availability
  7. Custom Protection Rules
  8. Key Takeaways
  9. Complete Working Example

What Are Environments?

An environment in GitHub Actions is a named object that represents a deployment target. Common names are preview, staging, and production, but you can name them anything that reflects your infrastructure.

Each workflow job can reference at most one environment. That reference does three things:

  1. Gives the job access to secrets and variables scoped to that environment.
  2. Activates any protection rules configured on the environment.
  3. Records the deployment in GitHub's deployment history, including the target URL if one is supplied.

Environments are defined per-repository under Settings → Environments. Each environment, with its own secrets, variables, and protection rules, is configured independently from the others.

Deployment Protection Rules

Protection rules act as gates: a job that references an environment will not start executing its steps until every protection rule on that environment has been satisfied. GitHub provides three built-in rules.

Required Reviewers

Up to six people or teams can be designated as required reviewers. When the workflow reaches a job that references the environment, it pauses and sends a notification to each reviewer. The job only proceeds after one of the designated reviewers approves — or is rejected outright, which cancels the job.

This pattern is common for production deployments where a second set of eyes is required before code reaches live systems.

Wait Timer

Specifies a delay in minutes (between 0 and 43,200, which equals 30 days) that must elapse after the job is triggered before it may run. A wait timer can act as a cooling-off period — for example, ensuring a canary deployment has been live for thirty minutes before the full rollout proceeds.

Deployment Branches

Restricts which branches are permitted to deploy to the environment. There are three options:

All branches

Any branch can deploy to this environment. Useful for non-production environments where all developers need access.

Protected branches only

Only branches that have branch protection rules configured in the repository can deploy here.

Selected branches

Only branches whose names match patterns you specify are permitted. For example, a pattern of release/* restricts deployments to branches that start with release/.

Protection rules compose: configuring all three on the same environment means a job must satisfy every rule before it can proceed.

Environment-Scoped Secrets and Variables

Secrets and variables that you add to an environment are separate from repository-level and organisation-level secrets and variables. They are only accessible to jobs that reference that specific environment.

A common pattern is to keep the same secret name across environments — for example, DEPLOY_TOKEN — but store a different value in each environment. The correct value is injected automatically depending on which environment the job targets.

# In the deploy-preview job
environment:
name: preview
steps:
- run: ./scripts/deploy.sh --token "${{ secrets.DEPLOY_TOKEN }}"
# secrets.DEPLOY_TOKEN resolves to the preview environment's value

# In the deploy-production job
environment:
name: production
steps:
- run: ./scripts/deploy.sh --token "${{ secrets.DEPLOY_TOKEN }}"
# secrets.DEPLOY_TOKEN resolves to the production environment's value

The same scoping applies to variables. A variable named API_VERSION can hold 2.3.0-beta in the preview environment and 2.2.1 in production, letting the two deploy jobs use appropriate version identifiers without any conditional logic in the workflow file.

Referencing an Environment in a Job

Add an environment: block inside the job definition. The minimal form requires only the environment name:

jobs:
deploy:
runs-on: ubuntu-latest
environment: production # short form — name only

The extended form also accepts a url: key, which GitHub displays as a link on the deployment status summary:

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://catalogue.example.com/api/v${{ vars.PROD_VERSION }}

It is possible to make the URL dynamic using expressions — combining a base URL stored in an environment variable with a version number from an environment variable, for instance. GitHub renders the final resolved URL as a clickable link on the Actions run summary page.

The Environment URL

The deployment URL appears in GitHub's deployment history and on the workflow run summary. It serves as a direct link to the running application after a successful deployment, making it easy for reviewers or stakeholders to validate the result without searching for the address manually.

environment:
name: preview
url: ${{ vars.PREVIEW_URL }}/api/v${{ vars.PREVIEW_VERSION }}
# PREVIEW_URL = https://preview.catalogue.internal
# PREVIEW_VERSION = 3.1.0-rc.4
# Resolved URL: https://preview.catalogue.internal/api/v3.1.0-rc.4

Both vars.* and secrets.* can be used in the URL expression, though embedding secrets in a URL that will appear in logs is not recommended. Prefer variables for the URL components.

Repository Availability

Environments can be created and configured freely for public repositories. For private repositories, environment support is available on:

  1. Personal accounts with a GitHub Pro subscription.
  2. Organisations on the GitHub Team plan or higher.

On plans that do not support environments for private repositories, you can still reference an environment name in a workflow. GitHub will create the environment automatically on the first run, but without any secrets, variables, or protection rules configured, jobs that expect environment-scoped secrets will fail because the values do not exist.

Custom Protection Rules

Beyond the three built-in rules, GitHub supports custom deployment protection rules backed by third-party services. These are implemented as GitHub Apps that respond to a webhook event when a deployment is waiting. The app performs its own checks — such as querying a security scanner, verifying a change management ticket, or checking a feature flag system — and then calls back to GitHub to approve or reject the deployment.

Custom deployment protection rules require familiarity with GitHub Apps, webhooks, and the deployment callbacks API. As of early 2026 this feature is still in beta and subject to change.

Key Takeaways

  1. An environment is a named deployment target (e.g. preview, production) configured in repository settings. A job can reference at most one environment.
  2. Deployment protection rules — required reviewers, wait timer, and deployment branch restrictions — gate the job before any of its steps execute.
  3. Environment-scoped secrets and variables are separate from repository and organisation secrets. They are injected only into jobs that reference their environment.
  4. Reuse the same secret or variable name across environments (e.g. DEPLOY_TOKEN) and store different values per environment. The correct value is resolved automatically.
  5. The optional url: key on an environment block produces a clickable deployment link on the GitHub Actions summary page.
  6. Environments for private repositories require GitHub Pro (personal) or GitHub Team (organisation). Without the plan, the environment is created on first run but holds no secrets or protection rules.
  7. Custom protection rules integrate third-party approval systems via GitHub Apps and are currently in beta.

Complete Working Example

The workflow below demonstrates a three-job deployment pipeline for a Node.js API. One build job feeds two deployment jobs, each linked to a different environment with its own scoped secrets and variables. The production environment is intended to be configured with a required-reviewer protection rule in repository settings.

name: Deployment Environments - Book Catalogue API

# Trigger on pushes to either the preview branch (integration testing)
# or main (production releases). Each branch maps to a different environment
# so teams can validate changes before they reach production.
on:
push:
branches:
- main
- preview

jobs:
# ------------------------------------------------------------------
# build-and-test: Compile and validate the application before any
# deployment job runs. Separating build from deploy means a failed
# test blocks both deployment jobs without them having to duplicate
# the test logic.
# ------------------------------------------------------------------
build-and-test:
name: Build and Test
runs-on: ubuntu-latest

steps:
- name: Checkout source
uses: actions/checkout@v4 # pull down the commit that triggered the push

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # cache the npm dependency tree to speed up subsequent runs

- name: Install dependencies
# ci installs exactly what is in package-lock.json, preventing
# accidental version drift between local and CI environments.
run: npm ci

- name: Run unit tests
# Fail fast: if any test fails the job stops here and neither
# deploy-preview nor deploy-production will be allowed to run.
run: npm test

- name: Build distributable
run: npm run build # produces compiled output in the dist/ folder

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: catalogue-api # name used by download steps in the deploy jobs
path: dist/
# keep the artifact only long enough for the deploy jobs to consume it;
# old artifacts waste storage and add confusion.
retention-days: 1

# ------------------------------------------------------------------
# deploy-preview: Deploys only when the triggering push is on the
# preview branch. The `environment:` block links this job to the
# preview environment in repository settings, giving it access to
# PREVIEW_DEPLOY_TOKEN and PREVIEW_VERSION, which are scoped
# exclusively to that environment and unavailable to any other job.
# ------------------------------------------------------------------
deploy-preview:
name: Deploy to Preview
runs-on: ubuntu-latest
needs: [build-and-test] # wait for a successful build before deploying

# Guard: skip this job when the push is to main, not preview.
# Without this condition both deploy jobs would queue on every push.
if: github.ref == 'refs/heads/preview'

# Link the job to the preview environment. GitHub will apply any
# configured protection rules (e.g. deployment branch restrictions)
# before allowing the job's steps to execute.
environment:
name: preview
# The URL is shown on the deployment status page so reviewers and
# stakeholders can navigate directly to the deployed application.
url: ${{ vars.PREVIEW_URL }}/api/v${{ vars.PREVIEW_VERSION }}

steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: catalogue-api # must match the name used in the upload step
path: dist/

- name: Deploy to preview server
# PREVIEW_DEPLOY_TOKEN is an environment secret -- it is only available
# to jobs that reference the preview environment. This prevents the
# production token from accidentally being used in a preview deploy.
env:
DEPLOY_TOKEN: ${{ secrets.PREVIEW_DEPLOY_TOKEN }}
VERSION: ${{ vars.PREVIEW_VERSION }}
run: |
echo "Deploying catalogue-api version $VERSION to preview"
./scripts/deploy.sh --env preview --version "$VERSION" --token "$DEPLOY_TOKEN"

# ------------------------------------------------------------------
# deploy-production: Deploys only on pushes to main. The production
# environment in repository settings is configured with a required
# reviewer rule, so this job will pause and wait for an approval
# before any of its steps execute.
# ------------------------------------------------------------------
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [build-and-test] # same gate as deploy-preview

# Guard: only run this job for pushes to main.
if: github.ref == 'refs/heads/main'

# Link to the production environment. If a required-reviewer protection
# rule is configured in settings, the job will halt here until a designated
# reviewer approves the deployment via the Actions UI or the email prompt.
environment:
name: production
# PROD_URL and PROD_VERSION are environment variables exclusive to
# the production environment -- they differ from the preview values.
url: ${{ vars.PROD_URL }}/api/v${{ vars.PROD_VERSION }}

steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: catalogue-api
path: dist/

- name: Deploy to production server
# PROD_DEPLOY_TOKEN is scoped to the production environment only.
# Using a separate token per environment limits the damage if one
# environment's credentials are ever rotated or compromised.
env:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
VERSION: ${{ vars.PROD_VERSION }}
run: |
echo "Deploying catalogue-api version $VERSION to production"
./scripts/deploy.sh --env production --version "$VERSION" --token "$DEPLOY_TOKEN"

- name: Notify deployment tracking system
# Only runs after a successful deploy so the tracking record reflects
# the actual state of production, not an intended state.
env:
TRACKING_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
VERSION: ${{ vars.PROD_VERSION }}
run: |
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TRACKING_TOKEN" \
"https://tracker.internal/deployments" \
-d "{\"version\": \"$VERSION\", \"environment\": \"production\", \"actor\": \"${{ github.actor }}\"}"
Share this lesson: