Module 1: Why Cicd And Gitops
7. Hands-on: your first local workflow, end to end
Description
This is the moment where everything you read in lessons 1 through 5, and installed in lesson 6, turns into a workflow actually running. You're going to create a disposable repository —outside of andes-cargo-infra/, which you only start in lesson 8— write a "hello world" workflow, and run it with act: list its jobs, trigger it with a simulated event, and read its complete output. You're also going to reproduce, on purpose, two real act errors — so you recognize them immediately when they show up later in this guide, instead of the first time catching you off guard with no warning.
Connection to the module
This is the whole module's hinge lesson. Lesson 6 gave you the tool, ready; this lesson uses it, for the first time, on a real workflow. Everything you write here is intentionally disposable —a "hello world" with no connection to Andes Cargo— because the goal isn't to start the real project yet (that's lesson 8), it's for the complete cycle —writing YAML, listing jobs, triggering them, reading the output— to stick with you before you touch infrastructure that matters to someone.
The complete cycle, at a glance
sequenceDiagram
participant You
participant act
participant Docker
You->>act: act -l
act-->>You: list of jobs detected in .github/workflows/
You->>act: act push
act->>Docker: docker pull catthehacker/ubuntu:act-latest
Docker-->>act: image ready
act->>Docker: creates an ephemeral container for the job
Docker->>Docker: runs each step, in order
Docker-->>act: output of each step
act-->>You: "Job succeeded" (or the error, if something failed)
Two commands, each with a specific job: act -l reads the YAML and tells you what jobs exist and what events trigger them, without executing anything. act push (or plain act, if the workflow listens for push) simulates that event and runs the complete job, for real, inside a container.
The project: one repository, one workflow
Create a new folder for this exercise, outside any real project:
mkdir act-lab && cd act-lab
git init
mkdir -p .github/workflows
.actrc — the same line you already wrote in lesson 6, now in this specific project (remember: act looks for it in the current working directory, not globally):
-P ubuntu-latest=catthehacker/ubuntu:act-latest
.github/workflows/hello.yml — this lesson's only workflow:
name: hello-world
on:
push:
branches: [main]
jobs:
say-hello:
runs-on: ubuntu-latest
steps:
- name: Print a greeting
run: echo "Hello from act, running Andes Cargo's first workflow"
- name: Show reproducibility fields
run: |
echo "run_id=${{ github.run_id }}"
echo "run_number=${{ github.run_number }}"
echo "sha=${{ github.sha }}"
Notice the structure, even though Module 2 dissects it in full: on says when it runs (on every push to main), jobs groups the work (a single job, say-hello), runs-on says on which image (ubuntu-latest, the one .actrc maps to catthehacker/ubuntu:act-latest), and steps is the sequence of commands, in order. The second step prints three values you're going to use to confirm, with your own eyes, this guide's reproducibility table: github.run_id and github.run_number (fixed under act), and github.sha (your repository's real commit, which is going to vary).
Confirm act detects the workflow before running anything:
git add -A
git commit -m "first workflow"
act -l
What to expect (literal output, executed to write this lesson):
Stage Job ID Job name Workflow name Workflow file Events
0 say-hello say-hello hello-world hello.yml push
This table is purely informational: it tells you what jobs exist, in which file, and what event triggers them — without executing a single command yet.
Step 1 — Trigger the workflow with act push
act push
What to expect (literal output, executed to write this lesson — note before reading it: on a Mac with an Apple Silicon chip you're also going to see the architecture warning from lesson 6; it's omitted here for brevity, you already know it):
[hello-world/say-hello] ⭐ Run Set up job
[hello-world/say-hello] 🚀 Start image=catthehacker/ubuntu:act-latest
[hello-world/say-hello] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true
[hello-world/say-hello] using DockerAuthConfig authentication for docker pull
[hello-world/say-hello] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[hello-world/say-hello] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[hello-world/say-hello] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir=
[hello-world/say-hello] ✅ Success - Set up job
[hello-world/say-hello] ⭐ Run Main Print a greeting
[hello-world/say-hello] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir=
[hello-world/say-hello] | Hello from act, running Andes Cargo's first workflow
[hello-world/say-hello] ✅ Success - Main Print a greeting [57.549834ms]
[hello-world/say-hello] ⭐ Run Main Show reproducibility fields
[hello-world/say-hello] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir=
[hello-world/say-hello] | run_id=1
[hello-world/say-hello] | run_number=1
[hello-world/say-hello] | sha=d379bd25a7179b3b0e1f598cc891a8ee62abc04a
[hello-world/say-hello] ✅ Success - Main Show reproducibility fields [60.556458ms]
[hello-world/say-hello] ⭐ Run Complete job
[hello-world/say-hello] Cleaning up container for job say-hello
[hello-world/say-hello] ✅ Success - Complete job
[hello-world/say-hello] 🏁 Job succeeded
Read this output with the same attention you'd give a terraform plan. Every line with 🐳 is a real Docker operation —act doesn't simulate it, it executes it—; the docker create/docker run with entrypoint=["tail" "-f" "/dev/null"] is the technical trick act uses to keep the container alive while it runs each step inside it, one at a time, with docker exec. Every ✅ marks a successful step; if any of them failed, act would stop right there and report the failure, exactly like a real GitHub runner.
The reproducibility table, confirmed with your own eyes
Look at the three lines the second step printed:
| Field | This run's value | Fixed or variable? |
|---|---|---|
run_id | 1 | Fixed — act doesn't increment this value between local runs, unlike real GitHub |
run_number | 1 | Fixed, same reason |
sha | d379bd25a7179b3b0e1f598cc891a8ee62abc04a | Variable — it's the real commit from git rev-parse HEAD in your repository; yours is going to be a completely different hash |
This is, literally, this guide's finest honesty distinction, confirmed with a real command instead of just read about in the design: every time you run act push on this same repository, run_id and run_number are going to stay 1 — but if you run the workflow after a new commit, sha is going to change. No "What to expect" block in this guide presents a commit SHA as if it were a fixed value — it's always marked as variable, exactly like here.
Step 2 — Reproduce two real errors, on purpose
Error 1 — Badly indented YAML
Break your workflow's indentation on purpose. Create .github/workflows/broken.yml:
name: broken-workflow
on:
push:
jobs:
say-hello:
runs-on: ubuntu-latest
steps:
- run: echo "this yaml is broken"
Notice the error: runs-on lost its indentation relative to say-hello. Run:
act push
What to expect (literal output, reproduced for this lesson):
Error: workflow is not valid. 'broken.yml': yaml: line 7: mapping values are not allowed in this context
act validates YAML syntax before trying to execute anything — it doesn't even get to creating a container. The message gives you the exact file and line, although the line number may not exactly match where the error "looks" like it is at a glance (YAML is sensitive to indentation in a way that sometimes makes the error get reported a line before or after where the human eye first spots it). Delete broken.yml before continuing — you don't need it for the rest of this lesson.
Error 2 — An event that triggers no job
Your hello.yml only listens for push. What happens if you ask act to simulate a different event, one that no job in this repository expects?
act pull_request
What to expect (literal output, reproduced for this lesson):
Error: Could not find any stages to run. View the valid jobs with `act --list`. Use `act --help` to find how to filter by Job ID/Workflow/Event Name
This isn't a syntax error —your YAML is still valid— it's act telling you, precisely, that no job in this repository is configured to react to the pull_request event. It's the same kind of situation you're going to run into in Module 3, when ci.yml listens for pull_request and apply.yml listens for push — if you ask act for the wrong event for the wrong workflow, this is exactly the message you're going to see.
Common mistakes
Confusing the YAML error with a Docker problem (diagnosis-based). What happens: someone sees Error: workflow is not valid and starts checking whether Docker is running, whether the image downloaded correctly, etc. Why it happens: any act error feels, at first glance, like "something with the containers went wrong." How to spot it: if the message includes the phrase workflow is not valid followed by a filename and a line number. How to fix it: this error happens before act touches Docker at all — it's a YAML parsing error, fixed by editing the indicated file, never by restarting containers.
Forgetting that act -l doesn't execute anything (expectation-based). What happens: someone runs act -l, sees the jobs table, and expects that to mean the workflow already ran. Why it happens: the output superficially looks like an execution summary. How to spot it: if you expected to see your workflow's echo after running act -l, and it doesn't show up anywhere. How to fix it: -l is for "list" — it's purely informational, it reads the YAML without running a single step. To actually execute it, you need act push (or plain act, or act -j <job-id> for a specific job).
Expecting act pull_request to work on any repository (conceptual, see Error 2 above). What happens: someone assumes act <event> is a generic command that always runs "the workflow," regardless of which event that specific workflow listens for. Why it happens: it's easy to think of act as "the button that runs everything" instead of "the simulator for a specific event." How to spot it: if the Could not find any stages to run error surprises you when you request an event your YAML doesn't declare in its on block. How to fix it: always check act -l first — the Events column tells you exactly which event you need to ask act for so that job runs.
Exercises
Exercise 1 — Run the cycle from memory. Without looking at this lesson, write the two commands you used today, in order, and what each one does.
See solution
1. act -l — reads the YAML in .github/workflows/ and shows what jobs exist, in which file, and what event triggers them, without executing anything. 2. act push (or act <event> in general) — simulates that specific event and runs, for real, inside a Docker container, every job that listens for it, step by step, in order.
Exercise 2 — Predict which field changes. If you run act push on this lesson's same repository three times in a row, without making any new commit in between, what do you expect to happen with run_id, run_number, and sha on each run?
See solution
run_id and run_number are going to stay 1 all three times — act doesn't increment them between local runs, unlike real GitHub, where every run of a workflow has a distinct, growing run_id. sha is also going to be identical across all three runs, because you didn't make any new commit in between — github.sha under act reflects your local Git repository's real HEAD at the moment of the run, and if HEAD didn't change, neither does the value. sha would only change if you made a new commit before running act push again.
Exercise 3 — Diagnose the wrong event. A coworker shows you this message: "I ran act workflow_dispatch on my repository and got Could not find any stages to run, but I'm sure my YAML doesn't have any syntax errors." What command would you ask them to run first to diagnose the problem, and what would you expect to see there?
See solution
You'd ask them to run act -l first. That command shows, in the Events column, exactly which event(s) trigger each job in their repository. If the column says push (like in this lesson's hello.yml) and they requested workflow_dispatch, the diagnosis is immediate: there's no syntax error —they were right about that— they simply asked act for an event that no job in that repository listens for. The fix is either running act push (the correct event for that YAML), or adding workflow_dispatch to the workflow's on block if they really want to be able to trigger it manually — a pattern this guide's Module 2 covers in detail.
Summary and next step
In this lesson you ran, for the first time, a complete workflow with act: you listed its jobs with act -l (literal), triggered it with act push, and read its complete output, line by line, including the real Docker operations underneath. You confirmed with your own eyes this guide's design reproducibility table: run_id and run_number fixed at 1, sha variable based on your real commit. You also literally reproduced this layer's two most common errors: badly indented YAML, and requesting an event no job listens for.
Before moving on you should be able to: write a minimal "hello world" workflow from memory; explain the difference between act -l and act push; and read the Could not find any stages to run message without confusing it with a Docker problem.
You have the complete cycle down. All that's left is starting the real project. Lesson 8 —this module's project— prepares andes-cargo-infra/, the repository you're going to use, without rewriting it, all the way through Module 8's capstone.
Resources
- nektosact.com — User Guide — official documentation for
act -l,act push, and how to filter by event or job. - GitHub Docs — Understanding GitHub Actions — the anatomy of
on/jobs/steps/runs-onthat you started using in this lesson, dissected in full in Module 2. - YAML Spec — Indentation — the official YAML specification, the source of why indentation isn't a style preference, but mandatory syntax.