Deployment Environment

A deployment environment is a named target in GitHub Actions that carries its own secrets, variables, and protection rules such as required reviewers.

A deployment environment is a named deployment target in GitHub Actions that a job points at with the environment key, and that carries its own secrets, its own variables, and its own protection rules such as required reviewers. A job bound to an environment stays in a waiting state until every protection rule on that environment passes, and only then does it reach a runner with the environment's secrets and variables in scope.

The configuration lives in repository settings rather than in the workflow file. That split is the point: staging and production can demand different approvals and hand out different credentials while the YAML that references them stays one line apart.

Definition

An environment is created under Settings, then Environments, in a repository. Names are matched case insensitively, may not exceed 255 characters, and must be unique within the repository. A workflow that references a name nobody created causes GitHub to create that environment on the spot, with no protection rules and no secrets attached, which is why a typo in the name produces a green deploy that skipped every gate you thought you had.

An environment holds three kinds of thing.

Protection rules decide whether a job that references the environment is allowed to start. Environment secrets are encrypted values that reach only jobs referencing the environment, and only after its rules pass. Environment variables are plaintext values readable through the vars context, again only from jobs that reference the environment.

Secrets resolve at the lowest level that defines the name, so an environment secret wins over a repository secret and over an organization secret sharing that name. An environment stores up to 100 secrets (secrets reference, checked on 2026-08-13).

The environment key takes a single value per job, written either as a name string or as an object with name and url. A run that ships to staging and then to production therefore needs two jobs, each referencing its own environment and clearing its own rules.

The protection rules and their documented limits

Five rule types are available. Their limits are fixed by GitHub rather than chosen per repository (deployments and environments reference, checked on 2026-08-13).

RuleWhat it holds the job forDocumented limit
Required reviewersA listed person or team approves the jobUp to 6 users or teams; one approval releases the job
Wait timerA fixed delay after the job is triggered1 to 43,200 minutes (30 days); the wait is not billable
Deployment branches and tagsThe run's GITHUB_REF matches an allowed patternThree modes: no restriction, protected branches only, selected branches and tags
Allow administrators to bypassNothing while enabled; disabling it removes the admin overrideEnabled by default
Custom deployment protection rulesA GitHub App returns an approvalAny number installed per repository, 6 enabled per environment

Two details in that table cause most of the surprises. Branch and tag patterns are matched against GITHUB_REF, and a wildcard stops at a slash, so release/* matches release/1.4 while release/*/* is needed for release/1.4/hotfix. And required reviewers have a companion setting, prevent self-review, which stops the person who triggered the deployment from being the person who approves it.

Plan availability differs by rule. Environments, environment secrets, and deployment branches work in public repositories on all current plans, and in private or internal repositories on GitHub Pro, Team, or Enterprise. Required reviewers, wait timers, and custom protection rules are limited to public repositories for accounts on Free, Pro, and Team.

What referencing an environment records

A job that references an environment creates a deployment object carrying the environment name, then deployment status objects carrying the job state and, when the job supplies one, an environment_url. Those objects drive the deployments view on the repository and are readable through the REST API, so the deploy history for a service becomes queryable without parsing workflow logs (managing environments, checked on 2026-08-13). Setting deployment: false under the environment key borrows the secrets and variables while skipping the deployment object entirely.

Example

This workflow builds on every push to main, then hands the artifact to a second job bound to the production environment. The production environment has one required reviewer and a deployment branch rule allowing main only.

name: deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/build.sh
      - uses: actions/upload-artifact@v4
        with:
          name: bundle
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: production
      url: ${{ steps.release.outputs.url }}
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: bundle
          path: dist/
      - id: release
        run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
          API_HOST: ${{ vars.API_HOST }}

Follow one push through it. The build job starts immediately because it references no environment. The deploy job then enters a waiting state, and GitHub opens a deployment against production and notifies the reviewer. DEPLOY_TOKEN is unreadable during that wait, because environment secrets are released only once the rules pass.

StageDeploy job stateRunner occupiedsecrets.DEPLOY_TOKEN readable
build runningQueued behind needsNoNo
Branch rule checked against GITHUB_REFWaitingNoNo
Reviewer notified, decision pendingWaitingNoNo
Reviewer approvesIn progressYesYes
deploy.sh exits 0Success, deployment status setReleasedJob finished

The middle rows are the reason a gated deploy costs nothing while it waits. Approval time and wait timer time happen before the job is dispatched, so no runner is held open and no job minutes accrue. A pull request opened from a branch other than main never reaches those rows at all, because the branch rule rejects the deployment before the reviewer is asked.

Picking the environment at run time

The name value accepts an expression, with the github, inputs, vars, needs, strategy, and matrix contexts available. One job definition can therefore route itself to different environments and pick up different credentials:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: ${{ github.ref_name == 'main' && 'production' || 'staging' }}
    steps:
      - run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

A push to main resolves the name to production and inherits the reviewer requirement. A push to any other branch resolves to staging, which can carry a wait timer of zero and a different DEPLOY_TOKEN value under the same secret name.

FAQ

What does the environment key do in a job?

It binds one job to a named environment configured in repository settings. The job waits until every protection rule on that environment passes, then runs with that environment's secrets and variables in scope, and GitHub records a deployment object for the run.

Does an environment secret override a repository secret with the same name?

Yes. GitHub resolves a secret name at the lowest level that defines it, so for a job that references an environment, the environment value wins over a repository secret and over an organization secret of the same name. An environment can hold up to 100 secrets.

What happens if a workflow names an environment that does not exist?

GitHub creates it during the run with no protection rules and no secrets configured. Anyone who can edit a workflow file can create an environment this way, while configuring reviewers, timers, and branch rules on it requires repository admin access.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.