RO EN

Professional CI/CD with GitHub Actions (2): environments and approval gates

Professional CI/CD with GitHub Actions (2): environments and approval gates ✨ Imagine generată cu AI
Doru Bulubasa
02 September 2026
41 views

In the first part of the series, we built the foundation: a pipeline that builds once, runs tests, and produces a versioned artifact. We also established the key principle – build once, deploy many.

Now comes the part where that artifact actually goes somewhere. But not directly to production. We pass it in a controlled way through separate environments – dev, staging, production – with a human approval before the final step.

The question is not just how the code gets to production, but who presses the button and when.

🎯 What are GitHub Environments

An environment in GitHub is a logical deployment target – dev, staging, production – which has its own protection rules, its own secrets, and its own variables.

Practically, an environment gives you three things:

  • Protection rules – who must approve, how long to wait, from which branch deployment can be done.

  • Secrets and per-environment variables – the staging connection string is different from production’s, and each stays only in its own environment.

  • Deployment history – see which version is in each environment and who promoted it.


🛠️ How to create environments

In the repo, go to Settings → Environments → New environment. Create three: dev, staging, and production.

For dev, you put no restrictions – you want quick feedback. For staging, you can set light restrictions. For production, you put all guarantees: reviewers and branch restrictions. That’s the idea – the closer the environment is to customers, the stricter the gate.


🚦 Protection rules (approval gates)

Here lies all the value. On the production environment, you activate:

Required reviewers

Choosing up to 6 people (or teams) who must approve before the deployment starts. This is the approval gate: when the pipeline reaches the production job, it stops and waits for someone to press Approve. Until then, nothing happens.

Wait timer

A mandatory delay (from a few minutes up to 30 days) before the deployment continues. Useful when you want a safety window in which someone can cancel.

Deployment branches

You restrict from which branches deployment can be done to this environment. Typically: only main reaches production. This way, no one accidentally promotes a feature branch directly to customers.


🔐 Secrets and per-environment variables

Each environment keeps its own secrets and variables. A secret defined on production is visible only to a job running in the production environment – and only after passing the approval gate.

This elegantly solves a real problem: you no longer have a single global set of secrets accessible by any job. The production connection string is isolated in its environment.

What differs between environments is the configuration – secrets, connection strings, feature flags – not the artifact. The binary remains the same.

🧩 How to reference an environment in a workflow

You link a job to an environment with the environment property. When the job starts, GitHub automatically applies the protection rules of that environment:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-test
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app
          path: ./publish
      # ... deploy step

Notice actions/download-artifact – we don’t rebuild anything. We take exactly the artifact produced by the build job and put it in staging. This is build once, deploy many in practice.


🏗️ The complete pipeline: build → staging → production

Putting it all together. A build job that produces the artifact, a deploy to staging, then a deploy to production that waits for approval:

name: CI/CD

on:
  push:
    branches: [ main ]

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - run: dotnet build --configuration Release
      - run: dotnet test --configuration Release --no-build
      - run: dotnet publish ./src/Api/Api.csproj -c Release -o ./publish
      - uses: actions/upload-artifact@v4
        with:
          name: app
          path: ./publish

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-test
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app
          path: ./publish
      - name: Deploy to Azure (staging)
        uses: azure/webapps-deploy@v3
        with:
          app-name: my-app-staging
          package: ./publish

  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app
          path: ./publish
      - name: Deploy to Azure (production)
        uses: azure/webapps-deploy@v3
        with:
          app-name: my-app-prod
          package: ./publish

The flow is linear and controlled: build-test produces the artifact, deploy-staging puts it in staging (needs: build-test), and deploy-production waits on staging (needs: deploy-staging). Because production has required reviewers, the last job stops and waits for approval.

Note: the Azure login step itself was intentionally left out – in part 5 we will see how to connect fully without passwords, using Managed Identity and OIDC. Here we focus on the mechanics of environments.


👀 What approval looks like in practice

When the pipeline reaches the production job, the Actions tab shows the Review deployments button. The reviewer sees what is about to be promoted, can leave a comment, and presses Approve and deploy or Reject.

Until approval, the artifact waits quietly – tested, ready, but not installed in production. Exactly the control you want before a release.


✅ Some healthy rules

  • dev without restrictions, production with reviewers and branch restrictions – strictness increases towards customers.

  • Keep secrets in the environment, not globally – each environment sees only what it needs.

  • Never rebuild per environment – always promote the same artifact.

  • Use url in environment to have a direct link to the deployed environment in the run history.


Conclusion

Environments transform a simple deploy into a controlled process: the same artifact moves through dev → staging → production, each environment with its own secrets, and before production there is a real approval gate. Nothing reaches customers without someone consciously pressing the button.

In part 3 we go one level down: instead of creating Azure resources manually from the portal, we define them in code with Bicep – so deployment includes infrastructure as well, not just the application.