GitHub·7 min read·

Environments and Protection Rules in GitHub Actions

Use GitHub Actions environments to gate production deploys behind required reviewers, wait timers, branch restrictions and isolated environment secrets.

NB

Netbay Infrastructure Team

Netbay Engineering

On this page

Secrets that are visible to every job are a quiet disaster waiting to happen. GitHub Actions solves this with environments: named containers (staging, production) that bundle their own secrets, their own protection rules, and their own deployment history. When a workflow references an environment, the runner fetches only that environment's secrets and obeys its rules — nothing else sees them. This post shows you how to model deployment gates with environments and protection rules.

What an environment actually gives you

An environment is a named resource you create at the repository or organization level. Here is what attaching a job to a production environment buys you:

  • Secret isolation: each environment has its own secret set. A production API key lives only in the production environment, never exposed to pull requests or CI.
  • Protection rules: required reviewers, a mandatory wait timer, and branch restrictions that must be satisfied before the job runs.
  • Deployment tracking: GitHub records every release into the environment, visible on the environment page.
  • Deployment branches: you can restrict an environment so only specific branches or tags can deploy to it.

Required reviewers and the approval gate

The reviewer rule is the headline feature. Mark an environment as "required reviewers" and list the people or teams who must approve before the job runs. When the workflow reaches that job, GitHub pauses it and pages the reviewers with an approval prompt in the UI and via notifications. The job does not start until an approver clicks the button. This is the cleanest way to make "no deploys without sign-off" an enforced platform rule rather than a good intention.

You can also add a wait timer — for example, 15 minutes — that delays the job after approval, giving you a window to cancel if you spot a problem.

Branch protection for environments

Beyond reviewers, an environment can restrict which branches may deploy to it. Set the deployment branches rule to a branch name or a tag pattern. A job targeting a pull request branch will be blocked from a protected environment entirely. Combine this with the standard branch protection rules on the code itself, and you get a two-layer gate: the code must be mergeable, and the deploy must be approved.

Wiring an environment into a workflow

Environments are referenced per job, not per workflow. Add an environment key to the job, then reference that environment's secrets in its steps. A typical production deploy might look like this:

yaml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist
  deploy:
    runs-on: ubuntu-22.04
    needs: build
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - name: Deploy to server
        env:
          PROD_SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
          PROD_HOST: ${{ secrets.PROD_HOST }}
        run: |
          echo "deploying dist to $PROD_HOST"
          eval "$(ssh-agent -s)"
          echo "$PROD_SSH_KEY" | ssh-add -
          rsync -avz --delete dist/ deploy@$PROD_HOST:/var/www/app

Notice env values are loaded from the production environment's secrets. If you also had a staging job referencing environment: staging, it would pull staging's secrets instead — the same variable names, different values, zero cross-talk.

Secrets scoping: environment vs repository vs org

Secrets can live at three levels. Repository secrets are available to every job in the repository, environment secrets only to jobs targeting that environment, and organization secrets to any repository in the org that lists them. Prefer environment secrets for anything production-shaped and keep repository secrets minimal. This way a failed CI run cannot leak a production key, because CI jobs never target the production environment.

Deployment concurrency and queue behavior

Two runs racing to the same environment are a classic footgun. Environments can enforce concurrency: set jobs with the same concurrency group, and GitHub will either cancel the in-flight run or queue the newcomer, depending on the cancel-in-progress flag. For deploys you usually want the newest run to cancel the older one so an old build does not overwrite a fresh deploy:

yaml
jobs:
  deploy:
    runs-on: ubuntu-22.04
    environment: production
    concurrency:
      group: production-deploy
      cancel-in-progress: false
    steps:
      - run: echo deploying

With cancel-in-progress false, a new deploy waits for the current one to finish before running, preventing two deploy tools from fighting over the same server.

job: ci no environment job: deploy environment: production protection rules required reviewers wait timer branch restriction approve then deploy env secrets only repo secrets visible to every ci job isolated from ci

Takeaway

Environments are the correct home for environment-specific secrets and human gatekeeping, not an afterthought. Isolate production credentials in a production environment with required reviewers, restrict the branches that can deploy, and let concurrency rules serialize releases. When a deploy clears all those gates and needs a real server, deploy to an Ubuntu VPS you create on Netbay in less than a minute at netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance