GitHub Actions · lesson 1 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
Also part of: DevOps Git & GitHub
GitHub Actions: Workflows, Jobs, and a Real Failing Step
Run a real GitHub Actions workflow locally with act — no push, no account needed — and see exactly what a failing step's exit code does to the rest of the job.
By SkillByExample · Updated
Every real GitHub Actions run can be reproduced on your own machine — no push, no account —
with act, which runs the actual workflow YAML against real Docker containers.
Installing act
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -s -- -b /usr/local/bin
act --version
act version 0.2.89
A minimal workflow, actually run
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Say hello
run: echo "Hello from GitHub Actions"
act push -P ubuntu-latest=catthehacker/ubuntu:act-latest
[CI/build] ⭐ Run Set up job
[CI/build] 🚀 Start image=catthehacker/ubuntu:act-latest
[CI/build] ✅ Success - Set up job
[CI/build] ⭐ Run Main actions/checkout@v4
[CI/build] ✅ Success - Main actions/checkout@v4 [40.897398ms]
[CI/build] ⭐ Run Main Say hello
[CI/build] | Hello from GitHub Actions
[CI/build] ✅ Success - Main Say hello [164.605749ms]
[CI/build] ⭐ Run Complete job
[CI/build] ✅ Success - Complete job
[CI/build] 🏁 Job succeeded
-P ubuntu-latest=catthehacker/ubuntu:act-latest maps the ubuntu-latest runner label to a
Docker image that actually has the tools GitHub’s real runners ship with — without it, act
falls back to a minimal image many real workflows won’t run cleanly on. Each ⭐ Run line is a
real step actually executing, in order, inside the container.
Listing what a workflow would run, without running it
act --list -P ubuntu-latest=catthehacker/ubuntu:act-latest
Stage Job ID Job name Workflow name Workflow file Events
0 build build CI ci.yml push
The bug: a failing step stops the job
# .github/workflows/broken.yml
name: Broken
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail on purpose
run: exit 1
act push -W .github/workflows/broken.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[Broken/build] ⭐ Run Main Fail on purpose
[Broken/build] ❌ Failure - Main Fail on purpose [116.490278ms]
[Broken/build] exitcode '1': failure
[Broken/build] ⭐ Run Complete job
[Broken/build] ✅ Success - Complete job
[Broken/build] 🏁 Job failed
Error: Job 'build' failed
exit 1 failed the step, and the job stopped there — no step after it ran. act itself
exits non-zero too (this command’s shell would report a non-zero $?), which is exactly what
lets a real CI system detect the failure and block a merge on it.
Practice
1. Add a step after the failing one and confirm it never runs.
- name: Fail on purpose
run: exit 1
- name: This should not run
run: echo "you should not see this"
[Broken/build] ⭐ Run Main Fail on purpose
[Broken/build] ❌ Failure - Main Fail on purpose [113.923628ms]
[Broken/build] 🏁 Job failed
Error: Job 'build' failed
No ⭐ Run Main This should not run line anywhere in the output — confirms the step was
skipped entirely, not run-and-ignored.
2. Force a step to run even after an earlier failure.
- name: Fail on purpose
run: exit 1
- name: Always runs
if: always()
run: echo "cleanup happened"
[Broken/build] ⭐ Run Main Fail on purpose
[Broken/build] ❌ Failure - Main Fail on purpose [108.024836ms]
[Broken/build] ⭐ Run Main Always runs
[Broken/build] | cleanup happened
[Broken/build] ✅ Success - Main Always runs [103.078221ms]
[Broken/build] 🏁 Job failed
if: always() runs regardless of prior failures — the job as a whole still reports failed
(one step failed), but the cleanup step ran, which is exactly the pattern for a step that
must execute no matter what (like uploading logs) even when earlier steps broke.
3. Check act's own exit code after a failing run.
act push -W .github/workflows/broken.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest > /dev/null 2>&1
echo "exit code: $?"
exit code: 1
act propagates the workflow’s failure as its own process exit code — this is what a real CI
system’s own automation checks, not the human-readable log output.
Next: secrets and how their values are handled in logs.