GitHub Actions · lesson 5 of 7
Beginner
2Intermediate
4Also part of GitHub Actions
5- + Bash Error Handling from Bash
- + CI/CD Pipeline Patterns: Sequential, Parallel, and Staged from DevOps
- + Terraform CI/CD with GitHub Actions (plan/apply) from Terraform
- + A CI Security Gate: Gitleaks, Bandit and pip-audit in One Workflow from DevSecOps (AppSec in the pipeline)
- + Migrating a Jenkins Pipeline to GitHub Actions from Jenkins
Deployment Gates: needs Really Blocks, environment Doesn't (Under act)
A failed job genuinely stops needs: from running the next stage — but act runs straight through an environment: declaration with no approval gate at all, a real, documented substrate limitation worth knowing before trusting a local test of a production deploy.
By SkillByExample · Updated
needs: and environment: both sound like they gate whether a job runs — only one of them
is actually enforced by a local test.
needs: really blocks the next stage
# .github/workflows/deploy_progression.yml
name: DeployProgression
on: [push]
jobs:
staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "deployed to staging"
production:
needs: staging
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "deployed to production"
act push -W .github/workflows/deploy_progression.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[DeployProgression/staging] ⭐ Run Main echo "deployed to staging"
[DeployProgression/staging] | deployed to staging
[DeployProgression/staging] 🏁 Job succeeded
[DeployProgression/production] ⭐ Run Main echo "deployed to production"
[DeployProgression/production] | deployed to production
[DeployProgression/production] 🏁 Job succeeded
staging completes fully before production starts — no interleaving, unlike the matrix
lesson’s genuinely parallel jobs. needs: staging is a real dependency edge act reads
straight from the YAML and enforces exactly as written.
The bug: a failed dependency skips the next stage entirely
# .github/workflows/deploy_fail.yml
name: DeployFail
on: [push]
jobs:
staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: exit 1
production:
needs: staging
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "deployed to production"
act push -W .github/workflows/deploy_fail.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[DeployFail/staging] ⭐ Run Main exit 1
[DeployFail/staging] ❌ Failure - Main exit 1 [105.820234ms]
[DeployFail/staging] 🏁 Job failed
Error: Job 'staging' failed
No [DeployFail/production] line appears anywhere — production never started at all, not
“ran and failed.” needs: defaults to running a job only if everything it depends on
succeeded, and act enforces that exactly like GitHub’s real service does.
The limitation: environment: has no approval gate under act
# .github/workflows/deploy.yml
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "deploying to production environment"
act push -W .github/workflows/deploy.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[Deploy/deploy] ⭐ Run Set up job
[Deploy/deploy] ✅ Success - Set up job
[Deploy/deploy] ⭐ Run Main echo "deploying to production environment"
[Deploy/deploy] | deploying to production environment
[Deploy/deploy] ✅ Success - Main echo "deploying to production environment" [149.044277ms]
[Deploy/deploy] 🏁 Job succeeded
Straight through, no pause, no approval prompt — even though environment: production is
exactly the field that, on real GitHub Actions, ties a job to required-reviewer approval rules
if the repository has any configured for that environment name. This is a genuine, documented
substrate limitation: those protection rules live in GitHub’s own repository settings, not in
the workflow YAML, so act — which only ever reads the YAML file — has no way to know they
exist, let alone enforce them. A local act run passing is not evidence that a real
deployment’s approval gate works; that can only be verified against the real GitHub service.
Practice
1. Chain three environments (dev → staging → production) with needs and confirm strict ordering.
jobs:
dev:
runs-on: ubuntu-latest
steps: [{ run: echo "dev" }]
staging:
needs: dev
runs-on: ubuntu-latest
steps: [{ run: echo "staging" }]
production:
needs: staging
runs-on: ubuntu-latest
steps: [{ run: echo "production" }]
[Chain/dev] 🏁 Job succeeded
[Chain/staging] 🏁 Job succeeded
[Chain/production] 🏁 Job succeeded
Three stages, three real completions in strict order — each stage’s needs: only allows it to
start after the previous one actually finished successfully.
2. Confirm two jobs with no needs relationship run concurrently, unlike the chained ones above.
jobs:
notify-slack:
runs-on: ubuntu-latest
steps: [{ run: echo "notifying" }]
update-status-page:
runs-on: ubuntu-latest
steps: [{ run: echo "updating status page" }]
[Independent/notify-slack ] ⭐ Run Set up job
[Independent/update-status-page] ⭐ Run Set up job
Both ⭐ Run Set up job lines appear back to back with no dependency between them (act pads
the shorter job name so both log prefixes line up in a column) — jobs with
no needs: relationship run in parallel by default, the same as this lesson’s staging/
production would have if needs had been omitted.
3. Use needs with multiple jobs (fan-in) and confirm the downstream job waits for all of them.
jobs:
build-frontend:
runs-on: ubuntu-latest
steps: [{ run: echo "frontend built" }]
build-backend:
runs-on: ubuntu-latest
steps: [{ run: echo "backend built" }]
deploy:
needs: [build-frontend, build-backend]
runs-on: ubuntu-latest
steps: [{ run: echo "deploying both" }]
[FanIn/build-frontend] ✅ Success - Complete job
[FanIn/build-frontend] 🏁 Job succeeded
[FanIn/build-backend ] ✅ Success - Complete job
[FanIn/build-backend ] 🏁 Job succeeded
[FanIn/deploy ] ⭐ Run Main echo deploying both
[FanIn/deploy ] | deploying both
[FanIn/deploy ] 🏁 Job succeeded
needs: [build-frontend, build-backend] only starts deploy once both listed jobs have
succeeded — a list in needs means “wait for all of these,” not “wait for any one.”
That closes the track. The thread through all five lessons: a workflow file describes exactly
what will happen and nothing more — act proves that description is internally consistent
(steps run in order, matrices expand, needs gates correctly), but anything that depends on
GitHub’s own infrastructure outside the YAML — masking’s substring matching, workflow_call’s
real input validation, an environment’s approval rules — needs the real service to confirm, not
just a passing local run.