GitHub Actions Environments: Variables, Secrets, Approvals & Protection Rules
As applications transition through development, staging, and production, software organizations face a critical tension: maintaining immutable application artifacts while adapting runtime configurations and access controls for each target environment.
This chapter explores GitHub Actions Environments, demonstrating how to manage environment-specific variables and secrets, enforce deployment branch restrictions, configure required manual approvers, and integrate wait timers into deployment pipelines.
1. The Core Dilemma: Multi-Environment Delivery & "Build Once, Deploy Many"
Modern CI/CD pipelines adhere strictly to the principle of Build Once, Deploy Many. An application package—whether a Java JAR/WAR file, a binary, or a OCI container image—is compiled and tested once in an upstream job. The exact same immutable artifact is then promoted sequentially through downstream environments (e.g., Development Staging Production).
The Challenge of Environment Variance
While the artifact remains constant across all stages, the runtime environment around it changes radically:
- Infrastructure Scale: A development environment might run on 2 static Kubernetes pods with minimal resource limits. Production might rely on Horizontal Pod Autoscalers (HPA) managing dozens of dynamic instances.
- Storage & Hardware: Development databases might use standard spinning hard disk drives (HDDs), whereas production demands high-IOPS Solid State Drives (SSDs) or cloud-managed multi-region clusters.
- Application Metadata: API endpoints, debug logging flags, ingress URLs, and external integration webhooks vary by environment (e.g.,
app.dev.example.comvs.app.example.com). - Enterprise Rigor: Non-critical applications may only require
devandprodenvironments. Enterprise banking applications often require dedicated environments forQA,UAT(User Acceptance Testing),Performance, andDisaster Recovery (DR).
Mental Model: The Artifact vs. Metadata Matrix Think of the container image as a standardized shipping container. The contents inside the container (the compiled binary) do not change regardless of destination. However, the shipping manifest, delivery route, receiving warehouse credentials, and security clearance checks (runtime configuration) change at every port of call.
2. Repository-Level vs. Environment-Level Scoping
The Naive Approach: Variable Prefixing
Without environment abstraction, engineers must store all variables and secrets at the flat Repository level. To prevent variable key collisions across environments, every key must be manually prefixed or suffixed:
DEV_APP_URL="app.dev.example.com"
DEV_DEBUG_MODE="true"
STAG_APP_URL="app.stage.example.com"
STAG_DEBUG_MODE="false"
PROD_APP_URL="app.example.com"
PROD_DEBUG_MODE="false"
This naive pattern introduces severe friction:
- Workflow Pollution: Workflows must contain complex conditional logic simply to resolve which variable name to reference (
vars.DEV_APP_URLvs.vars.PROD_APP_URL). - Naming Inflexibility: As environments scale from 3 to 10, variable naming schemes become brittle and prone to human error.
- Governance Deficit: Repository secrets grant universal access to any job running in the workflow, offering no mechanism to prevent a feature branch from referencing production credentials.
The Solution: GitHub Actions Environments
GitHub Actions Environments provide a dedicated logical scope for defining configuration data. By defining environments named dev and prod, configuration keys remain clean and generic across all workflows:
Workflows reference standard key names (vars.APP_URL, vars.DEBUG_MODE), while GitHub dynamically injects the appropriate contextual values based on the job's targeted environment.
3. Workflow Execution Architecture
When a workflow execution job binds to a GitHub Environment, the runner evaluates rules and context in a strict, sequential pipeline:
4. Scenario Setup & Application Code Base
To observe environment functionality in action, consider a minimalist microservice built with Python and Flask, packaged via Docker, and published to Docker Hub.
1. Application Code (app.py)
from flask import Flask
import os
app = Flask(__name__)
@app.route("/")
def hello():
return "Welcome to Cloud with VarJosh platform GitHub Actions runtime Docker + Flask"
@app.route("/health")
def health():
return "healthy"
if __name__ == "__main__":
# Default port 5000
app.run(host="0.0.0.0", port=5000)
2. Dependency Specification (requirements.txt)
flask
3. Container Definition (Dockerfile)
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py /app/
# Expose port 5000 for documentation purposes
EXPOSE 5000
CMD ["python", "app.py"]
5. Configuring Environments, Secrets, and Variables
1. Repository-Level Configuration
Repository secrets and variables are available globally across all jobs unless explicitly overridden by an environment scope.
- Repository Variable:
DOCKERHUB_USERNAME=cloudwithvarjosh - Repository Secret:
DOCKERHUB_TOKEN=<Docker_Hub_Personal_Access_Token>
2. Environment Definitions
Two logical environments are configured under Settings Environments:
Conceptually: If an environment variable and a repository variable share the exact same key name, the Environment Variable takes precedence during job execution.
6. Comprehensive GitHub Actions Workflow Implementation
The following workflow file (.github/workflows/01-environments-demo.yml) demonstrates how to construct multi-environment deployments targeting develop and main branches.
name: 01 GitHub Actions environments demo
on:
push:
branches:
- develop
- main
- 'feature/*'
jobs:
# -------------------------------------------------------------
# Job 1: Build Once and Publish Artifact to Registry
# -------------------------------------------------------------
build-and-publish:
name: Build and Publish Image
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ vars.DOCKERHUB_USERNAME }}/flask-app:latest
${{ vars.DOCKERHUB_USERNAME }}/flask-app:${{ github.sha }}
# -------------------------------------------------------------
# Job 2: Deploy to Development Environment
# -------------------------------------------------------------
deploy-dev:
name: Deploy to Development
needs: build-and-publish
if: github.ref_name == 'develop'
runs-on: ubuntu-latest
environment: dev
steps:
- name: Display Branch Information
run: echo "Deploying from branch ${{ github.ref_name }}"
- name: Display Environment Configuration
run: |
echo "Target Application URL: ${{ vars.APP_URL }}"
echo "Debug Mode Active: ${{ vars.DEBUG_MODE }}"
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Execute Development Deployment & Smoke Test
run: |
echo "Pulling container image..."
docker run -d --name dev-app -p 5000:5000 ${{ vars.DOCKERHUB_USERNAME }}/flask-app:${{ github.sha }}
sleep 3
echo "Executing Smoke Test..."
curl http://localhost:5000/health
echo ""
curl http://localhost:5000/
- name: Display Container Logs
run: docker logs dev-app
# -------------------------------------------------------------
# Job 3: Deploy to Production Environment
# -------------------------------------------------------------
deploy-prod:
name: Deploy to Production
needs: build-and-publish
if: github.ref_name == 'main'
runs-on: ubuntu-latest
environment: prod
steps:
- name: Display Branch Information
run: echo "Deploying from branch ${{ github.ref_name }}"
- name: Display Environment Configuration
run: |
echo "Target Application URL: ${{ vars.APP_URL }}"
echo "Debug Mode Active: ${{ vars.DEBUG_MODE }}"
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Execute Production Deployment & Smoke Test
run: |
echo "Pulling container image..."
docker run -d --name prod-app -p 5000:5000 ${{ vars.DOCKERHUB_USERNAME }}/flask-app:${{ github.sha }}
sleep 3
echo "Executing Production Smoke Test..."
curl http://localhost:5000/health
echo ""
curl http://localhost:5000/
- name: Display Container Logs
run: docker logs prod-app
Architectural Notes on Workflow Design
- Runner Isolation: Notice that
docker/login-actionmust be executed separately insidebuild-and-publish,deploy-dev, anddeploy-prod. Each job executes on an isolated Virtual Machine runner. Authentication state from Job 1 does not persist into Job 2 or Job 3. ifCondition vs.environmentScope:if: github.ref_name == 'develop'ensures the job is scheduled only when code is pushed todevelop.environment: devtells GitHub Actions to pull configuration context (variables/secrets) defined under thedevenvironment and apply its deployment protection rules.
7. Governance & Environment Protection Rules
To enforce compliance, GitHub environments provide critical governance gates.
Note on GitHub Plan Restrictions: In private repositories, Environment Protection Rules (such as Required Reviewers and Wait Timers) require a GitHub Enterprise or GitHub Team account. For personal, free-tier GitHub accounts, these protection features are available when applied to Public Repositories.
1. Deployment Branch Restrictions
Restricts environment deployments exclusively to designated branches or tags, preventing rogue pushes on feature branches from deploying to critical stages.
devEnvironment Config: Configured to accept deployments only from thedevelopbranch.prodEnvironment Config: Configured to accept deployments only from themainbranch.
2. Required Reviewers (Manual Approvals)
Blocks execution of a job associated with an environment until designated individual users or teams review and approve the run via the GitHub UI.
- Approvers can inspect the pipeline run, verify commit details, and leave mandatory tracking comments (e.g., Change Request ticket numbers like
CR-6767). - Option: Prevent Self-Review: Guarantees that the engineer who triggered the pipeline run cannot approve their own deployment.
3. Wait Timers
Configures a mandatory delay (from 1 minute up to 30 days) before allowing a job to run after it has been triggered.
- Operational Utility: Provides a buffer window for operational teams to receive automated notifications (e.g., Slack alerts) and cancel an accidental or flawed release before changes affect runtime infrastructure.
8. Complete Execution Walkthrough
Scenario A: Continuous Integration Push to develop Branch
- Developer pushes code to the
developbranch. - Workflow triggers
build-and-publishjob: Compiles image and tags it ascloudwithvarjosh/flask-app:<commit_sha>. deploy-prodevaluatesif: github.ref_name == 'main'Evaluated to False (Skipped).deploy-devevaluatesif: github.ref_name == 'develop'Evaluated to True.- GitHub verifies branch restriction for
devenvironment (developmatches allowed rules). - Job injects
devvariables (APP_URL="app.dev.cwvj.io",DEBUG_MODE="true"). - Runner starts, pulls the container artifact, executes smoke tests, and logs completion.
Scenario B: Production Release Push to main Branch
- Release engineer pushes or merges PR into
main. - Workflow triggers
build-and-publishjob (Build once). deploy-devevaluatesifcondition Skipped.deploy-prodevaluatesifcondition Evaluated to True.- Phase 1 Protection Check (Wait Timer): Workflow enters a
Waitingstate. A 1-minute countdown timer begins. - Phase 2 Protection Check (Required Reviewers): Once the wait timer elapses, GitHub prompts designated reviewers. Execution halts until an approver submits approval (with comment
Approved for release CR-6767). - Job injects
prodvariables (APP_URL="app.cwvj.io",DEBUG_MODE="false"). - Runner pulls image, executes smoke tests, and finishes production deployment.
9. Synthesizing Workflow Control Mechanisms
GitHub Actions provides multiple features to control workflow execution, job ordering, and environment security. The table below synthesizes their distinct roles:
10. Summary and Key Takeaways
- Build Once, Deploy Many: Always package application artifacts (JARs, OCI images) once in upstream jobs, promoting identical immutability across
dev,staging, andprod. - Variable Scope Isolation: Environments eliminate fragile variable prefixes (
DEV_APP_URL) by binding abstract key names (APP_URL) to logical environment contexts. - Precedence Hierarchy: Environment-level secrets and variables automatically override repository-level defaults when assigned to a job via the
environment:key. - Decoupled Governance: Job
if:conditions control workflow branching logic, whereas theenvironment:keyword enforces compliance access controls and variable injection. - Deployment Branch Protections: Restrict production environment credentials to secure long-lived branches (e.g.,
mainorrelease/*), preventing feature branch contamination. - Human-in-the-Loop Approval: Required Reviewers provide manual approval gates, enforcing change management tracking and audit compliance before deployment execution.
- Wait Timer Buffers: Timer delays establish automated operational safety windows, allowing pipelines to notify stakeholders and remain cancelable before modifying live environments.
- Runner Ephemerality: Every job executes on an isolated runner. Authentications, local file caches, or Docker context created in one job do not persist to downstream deployment jobs.