Module 4: Secrets Environments And Identity

6. GitHub Environments: `dev` and `prod`

Description

A GitHub Environment is a third control mechanism, different from everything you've seen so far: it doesn't protect a credential (that's what Secrets already do, lesson 3), and it doesn't replace the need for a credential (that's what OIDC does, lessons 4-5) — it controls when a job has permission to run, with a real person in the loop if configured that way. This lesson describes the complete mechanism, with the real Settings → Environments click-path. The required approval part is representative —act, verified, completely ignores this protection— but you're going to test that yourself, with a real act run, exactly that: the job runs anyway, without waiting for anyone.

Connection to the module

Lesson 3 previewed that a Secret can live inside an Environment, with different values for dev and prod. This lesson builds the complete mechanism those Secrets are part of: what an Environment is, how it's declared in a job, and its most important function for the rest of this guide — the required reviewers Module 5 is going to need when apply.yml runs terraform apply against real infrastructure, not just against a review plan.


Analogy: the two-key safe

Think of an Environment with required reviewers as a safe needing two different keys, turned at the same time, to open — one person alone, no matter how much they trust their own judgment, physically can't open it. The pipeline wanting to run terraform apply against prod is the first key: it already decided it wants to act. The second key is a designated person —someone other than whoever wrote the change, in the strictest configuration— who has to specifically look at that change and turn their own key for the door to open. Neither key, on its own, does anything.


What an Environment is: the real click-path

Like lesson 3's Secrets, this describes github.com's real interface — platform configuration, not a workflow, so there's no YAML act can interpret for this part:

   github.com/your-username/andes-cargo-infra
   └── Settings
       └── Environments (left sidebar menu)
           └── [New environment]
               Name: production
               └── Configuration rules
                   ├── Required reviewers
                   │   └── [+] Add people or teams
                   │       (the job PAUSES here until one of
                   │        them approves, from a real notification)
                   ├── Wait timer
                   │   └── minutes of mandatory wait before running
                   │       (even with no reviewer, as a time safeguard)
                   ├── Deployment branches
                   │   └── restricts WHICH branches can deploy to this
                   │       Environment (for example, only `main`)
                   └── Environment secrets (lesson 3)
                       └── values visible ONLY to jobs declaring
                           this Environment

A typical repository for this guide would have two Environments: dev (no required reviewers, for fast iteration) and production (with required reviewers, so no apply against real infrastructure runs without a person actively confirming it).


How it's declared in a job

jobs:
  terraform-apply:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Terraform apply
        run: terraform apply -auto-approve -input=false

The environment: production line is all the syntax needed on the YAML side — the rest of the behavior (pausing, notifying reviewers, waiting for their approval) lives entirely in the Settings → Environments configuration above, not in the file. This is deliberate: the same apply.yml, without changing a single line, behaves differently depending on how the Environment it points to is configured —no required reviewers and it runs immediately; with required reviewers, it pauses.


The representative part, with the exact quote

act completely ignores a job's environment: field. This isn't a guess or "probably true" behavior — it's confirmed against an open issue in act's own repository:

nektos/act issue #1714 — "Deployment Environments support" Status: open, labeled confirmed/not-planned. The original report explains: "In GitHub action it is possible to specify an 'environment' key to discriminate between divergent secrets set" — and confirms that act, verified, doesn't process that field at all: not to differentiate Secrets by environment, nor —the direct consequence that matters here— to apply any approval protection. The job runs exactly the same as if environment: didn't exist in the YAML.

confirmed/not-planned is the most honest label an open source maintainer can put on an issue: it's not a bug someone's going to fix soon — it's a recognized limitation, with no declared intention to solve it, because implementing the complete Environments model (including waiting for a real human approval, with notifications, with a web interface) is outside the scope of a tool that runs workflows locally, with no user backend or notifications at all.


Test it yourself: act running a production job asking for nothing

You're going to confirm this with a real run, not just the quote above. On andes-cargo-infra/, create a deliberately simple workflow, just for this test:

.github/workflows/environment-check.yml:

name: environment-check

on: workflow_dispatch

jobs:
  deploy-to-production:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: This step should only run after a human approves the "production" environment
        run: echo "Job ran without waiting for any required reviewer."

If this repository existed on real github.com, with production configured with at least one required reviewer, triggering this workflow should pause the job in a "Waiting" state until someone approves from the web interface — the job wouldn't even start running. Confirm it with act:

act workflow_dispatch -j deploy-to-production

What to expect (literal output, executed to write this lesson):

[environment-check/deploy-to-production] ⭐ Run Set up job
[environment-check/deploy-to-production] 🚀  Start image=catthehacker/ubuntu:act-latest
[environment-check/deploy-to-production]   ✅  Success - Set up job
[environment-check/deploy-to-production] ⭐ Run Main This step should only run after a human approves the "production" environment
[environment-check/deploy-to-production]   | Job ran without waiting for any required reviewer.
[environment-check/deploy-to-production]   ✅  Success - Main This step should only run after a human approves the "production" environment [55.2065ms]
[environment-check/deploy-to-production] ⭐ Run Complete job
[environment-check/deploy-to-production]   ✅  Success - Complete job
[environment-check/deploy-to-production] 🏁  Job succeeded

The complete command, start to finish, took 1.6 seconds. There's no pause, no "Waiting for approval" message, no interface to check — act doesn't even recognize environment: production is declared in the YAML. This is exactly the practical confirmation of the issue cited above: act runs the job as if that line didn't exist.


Why this isn't a flaw in this guide, but the declared boundary

That act ignores environment: doesn't mean the human-approval mechanism is useless or that this guide can't teach it — it means, specifically, that the block can't be demonstrated under act, only described precisely, as this lesson did. The distinction matters: Module 5 is going to build apply.yml with environment: production declared exactly like this, following the real pattern — that lesson's YAML is going to be 100% portable to a real GitHub repository, where the block would actually work. The only thing you can't do, within this guide, is watch that block happen with your own eyes — for that, Module 8, lesson 5, points to the option (never required) of creating a real GitHub repository.


Common mistakes

Concluding that, since act ignores it, environment: is useless for the rest of this guide (scope-based). What happens: someone, after seeing the job run with no pause, decides declaring environment: production in apply.yml (Module 5) is unnecessary, since "act doesn't respect it anyway." How to spot it: if your reasoning is "it does nothing here, so I won't declare it." How to fix it: declaring environment: production in apply.yml remains correct and necessary, precisely because that same file is what would run, with no change, in a real repository — where environment: would actually block the job until approval. Omitting it "because it does nothing here" would produce an incomplete apply.yml that would fail to apply the real control the day it's used in production.

Confusing required reviewers with the trigger's branches: (syntax-based). What happens: someone tries to achieve the same "only applies with review" effect by restricting on: push: branches: [main], without declaring any environment:. Why it happens: both mechanisms sound like "control over what can run." How to spot it: if your only deployment control is a branch filter, with no explicit human approval in the middle. How to fix it: branches: (Module 2, lesson 3) controls which event triggers the workflow — it involves no person reviewing at the moment. An Environment's required reviewers is a completely different control: it pauses the job, already triggered, until someone actively approves. The first is an automatic filter; the second is a real-time human intervention. apply.yml, in Module 5, is going to use both at once, each solving a different problem.


Exercises

Exercise 1 — Reproduce the test with dev instead of production. Change environment: production to environment: dev in environment-check.yml and run act workflow_dispatch -j deploy-to-production again. Does anything change in the behavior? Should it, based on what you learned in this lesson?

See solution

Nothing should change, and nothing does: act ignores the environment: field regardless of its value —dev, production, or any made-up name— because it simply doesn't process it at all (issue #1714). The only real difference between dev and production, on a real GitHub repository, would be in how each is configured in Settings → Environments —whether dev has no required reviewers and production does— not in the Environment's name itself.

Exercise 2 — Explain the difference between "confirmed" and "not-planned" in the issue's label. Issue #1714 is labeled confirmed/not-planned. Explain, in your own words, what each half of that label communicates separately.

See solution

confirmed means the project's maintainers acknowledge the reported behavior is real —it's not a user misunderstanding nor something already resolved. not-planned means that, while acknowledging it's real, there's no declared intention to implement it as a future project feature. Together, the two labels communicate exactly the level of honesty this lesson needs: it's not a bug that's going to disappear in the next act version, it's a recognized, permanent design limitation, solid grounds for labeling this part of the guide representative with confidence, not as "not yet verified."

Exercise 3 — Design Andes Cargo's prod Environment. Without writing YAML yet (that's Module 5), describe in prose what configuration rules you'd set on andes-cargo-infra/'s production Environment if this repository were real, using at least three of the four rules mentioned in this lesson's click-path.

See solution

A reasonable configuration: required reviewers with at least one person other than whoever wrote the change (so no author approves their own apply against production); deployment branches restricted only to main (so no feature branch, not even by accident, can trigger a deployment to production); and environment secrets with Andes Cargo's real production account credentials, completely separate from the ones the dev Environment would use — so a misconfiguration in dev can never accidentally expose a production credential.


Summary and next step

In this lesson you saw what a GitHub Environment is, the real click-path to configure it (Settings → Environments, required reviewers, wait timer, deployment branches), and how it's declared in a job (environment: <name>). You confirmed, with a real act run —not just the issue's quote— that act completely ignores this protection: the job ran in 1.6 seconds, with no pause, with no approval waited for. And you understood why this doesn't invalidate the mechanism for the rest of this guide: apply.yml, in Module 5, still declares environment: production because that YAML has to be portable to a real repository, where the block would actually happen.

Before moving on you should be able to: trace the complete click-path to create an Environment with required reviewers; explain, with the exact quote, why act ignores environment:; and explain why declaring environment: remains correct even though act doesn't respect it.

This closes out this module's conceptual, representative part. Lessons 7 and 8 return to fully executed ground: you're going to migrate Andes Cargo's real ci.yml to use Secrets instead of credentials written in the file, with real act output confirming every step.

Resources

  1. GitHub Docs — Using environments for deployment — complete official documentation for Environments, including required reviewers, wait timer, and deployment branches.
  2. nektos/act issue #1714 — the exact source for this lesson's quote: act ignores environment:, confirmed and not-planned.
  3. nektosact.com — User Guide — documentation for act workflow_dispatch, used in this lesson's experiment.