About Environments
Every pipeline you’ve built so far in this course runs the moment its trigger fires and does whatever the YAML says, no questions asked. That’s fine for linting and testing — you want fast feedback, not a committee meeting. But the moment a workflow actually pushes something live, “no questions asked” stops being a feature and starts being a liability. This section is about deployment, and deployment is where GitHub Actions gives you a dedicated concept for slowing things down on purpose: Environments.
What an Environment Actually Is
A GitHub Environment is not a server, a cloud account, or a physical place your code runs. It’s a named target you declare inside your repository — production, staging, testpypi, whatever makes sense — that a job can point at. The moment a job says environment: production, GitHub attaches three things to that job run:
- Protection rules that can pause the job before it executes
- Secrets and variables that only exist inside that environment, invisible to jobs that don’t reference it
- A deployment record, visible on the repo’s “Environments” and “Deployments” pages, optionally carrying a live URL
None of this changes what your job does. It changes what has to happen before your job is allowed to run, and it changes what your job is allowed to see. Think of an Environment as a gate with a name tag, not a place.
flowchart LR
A[Workflow triggered] --> B[Job references<br/>an environment]
B --> C{Protection rules<br/>satisfied?}
C -- No, waiting --> C
C -- Yes --> D[Job runs with that<br/>environment's secrets]
D --> E[Deployment recorded<br/>on Environments tab]
Declaring One
You don’t need a separate API call or a config file to create an Environment — you reference it from a job, and GitHub creates it the first time that workflow runs (or you can pre-create it manually):
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "deploying"That one line, environment: production, is the entire hook. Everything else — reviewers, secrets, wait timers — gets configured afterward in Settings → Environments on the repository, not in the workflow file. The workflow only ever references the environment by name; the rules live with the repo.
Note
Environment names are case-sensitive and workflow-file references must match exactly. A job pointed at Production will silently create a brand-new environment called Production, separate from production, instead of erroring.
Protection Rules: Making a Job Wait
This is the part that makes Environments worth learning. From the Environment’s settings page you can attach rules that GitHub Actions enforces automatically, with zero YAML:
- Required reviewers — up to 6 people or teams; the job pauses in a “Waiting” state until one of them approves it from the Actions run page. It won’t time out on its own; someone has to look at it.
- Wait timer — a fixed delay (in minutes) before the job is allowed to proceed, even with no human involved. Useful as a “you have 10 minutes to hit cancel if this was a mistake” buffer.
- Deployment branch/tag rules — restrict which branches or tags are even eligible to deploy to this environment at all, so a random feature branch can’t accidentally target
productionno matter what the workflow file says.
These rules stack. A production environment with both a required reviewer and a 10-minute wait timer means: someone approves it, then the clock starts, then the job runs. You’ll configure exactly this combination in a later chapter of this section.
Environment Secrets and Variables
Every environment gets its own secret and variable store, separate from the repository-level ones you’ve used up to now. A secret named API_KEY at the repository level and a secret also named API_KEY inside the production environment are two different values — the environment-scoped one wins for any job that references that environment.
This matters more than it sounds like. It means a staging job and a production job can both reference secrets.DATABASE_URL in identical YAML and get two completely different connection strings, because each job points at a different environment. You’re not templating secret names per stage; you’re letting the environment resolve the right value for you.
Tip
Environment secrets are only decrypted and injected once the job has cleared that environment’s protection rules. A workflow waiting on reviewer approval genuinely cannot read the secret yet — it’s not just hidden from logs, it isn’t in the job’s process environment at all until approval happens.
Environment URLs on the Deployments Tab
A job can report back where it deployed to, which GitHub then surfaces on the repo’s “Environments” sidebar and on the pull request itself:
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- run: echo "deploying"Once this job runs, anyone looking at the repo sees a clickable “View deployment” link pointing at that URL, plus a timestamped history of every deployment to that environment — who triggered it, which commit, whether it succeeded. This is genuinely useful for anything beyond a solo project: it’s a running log of “what’s live where” that nobody had to write down by hand.
The url doesn’t have to be static text — it’s commonly set dynamically from a previous step’s output (say, a Fargate service endpoint you don’t know until the deploy step finishes), which you’ll see in later chapters.
Where This Shows Up for the Rest of This Section
The next few chapters deploy the same tiny project to four different real destinations — GitHub Pages, AWS S3 + CloudFront, TestPyPI, and an EC2 instance — and each one will attach an environment: block so you can watch the Deployments tab fill in as you go. None of those early chapters need reviewers or wait timers; they’re single-target, low-stakes deploys. The protection-rule side of Environments — required reviewers, wait timers, multi-stage promotion — comes back later in this section once there’s a real multi-environment pipeline worth gating.