Module 2: Anatomy Of A Github Actions Workflow
3. Triggers: `push`, `pull_request`, `workflow_dispatch`
Description
Lesson 2 showed you that on is the first door an event has to cross. This lesson goes deeper into an infrastructure pipeline like Andes Cargo's three most important triggers: push (something reached a branch), pull_request (someone proposes a change), and workflow_dispatch (a human triggers it by hand). You're going to run all three, with act, on the same file, and you're going to discover —with real proof, not just reading about it— an important limit of what act can simulate: the branches:/paths: filters GitHub applies server-side.
Connection to the module
This lesson uses lesson 2's same anatomy-demo.yml, now looked at exclusively through its on block. Lesson 4 does the same with a fourth trigger, schedule, which deserves its own lesson for the cron syntax and for a different act limitation. Lesson 6 —hands-on— returns to pull_request with a hand-written event, richer than the one act generates by default.
Analogy: a shop's door bell
A trigger (on) is, literally, a shop's door bell: it defines what kind of entrance rings the bell, not what happens after it rings. push is the bell that rings every time someone comes in carrying a new box —a change that already reached a branch. pull_request is different: it rings when someone stands at the door proposing to bring something in, before that thing is actually inside —perfect for reviewing before accepting, which is exactly the pattern you're going to build in Module 3 with ci.yml. workflow_dispatch is the button someone from the shop can press manually, without anyone having walked through the door —"I want the bell to ring now, even though nobody came in yet."
The three triggers, on the same file
Going back to anatomy-demo.yml's on block (lesson 2):
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
push — something already reached a branch
act push
What to expect (literal fragment, already seen in full in lesson 2):
[andes-cargo-ci-demo/inspect-environment] | Event that triggered this run: push
push is the most direct trigger: it runs every time one or more commits reach a branch the filter allows. It's the right trigger for "something changed, it's already in the history" — the one you're going to use for apply.yml in Module 5, because an infrastructure apply should only ever run on code that already merged, never on a proposal still under review.
pull_request — someone proposes a change
act pull_request
What to expect (literal output, executed to write this lesson):
[andes-cargo-ci-demo/inspect-environment] | Event that triggered this run: pull_request
pull_request runs when a Pull Request against a branch the filter allows is opened, updated, or reopened —without that code having reached main yet. It's the trigger for half of the pipeline's CI (Module 3): terraform plan running on the proposed change, as evidence for whoever reviews it, before the possibility of merging it even exists.
workflow_dispatch — a human triggers it by hand
act workflow_dispatch
What to expect (literal output, executed to write this lesson):
[andes-cargo-ci-demo/inspect-environment] | Event that triggered this run: workflow_dispatch
workflow_dispatch doesn't wait for any Git event — on a real GitHub repository, it shows up as a "Run workflow" button in the Actions tab, which anyone with permissions can press whenever they want. You're going to use it in Module 5 to run the drift-detection job by hand, outside its scheduled time, when you want to confirm it without waiting for the next cron trigger.
This lesson's finding: act doesn't apply branches:/paths:
So far, the three triggers behaved exactly as you'd expect. But there's a question you haven't answered yet: what happens if you ask act for a push, and the event describes a branch the branches: [main] filter doesn't allow? This question matters a lot for Andes Cargo — it's, literally, the reason apply.yml is going to carry branches: [main] in Module 5: so it never runs on a feature branch.
A test workflow, with the exact same filter apply.yml is going to carry:
name: branch-filter-test
on:
push:
branches: [main]
jobs:
only-on-main:
runs-on: ubuntu-latest
steps:
- run: echo "This should only run for pushes to main"
A hand-written push event, describing a branch the filter doesn't allow:
{
"ref": "refs/heads/feature/add-shipment-tags",
"repository": { "default_branch": "main" }
}
act push -e push-feature-event.json -j only-on-main
What to expect (literal output, executed and verified on this machine, today, against act 0.2.89 — this lesson's central finding):
[branch-filter-test/only-on-main] ⭐ Run Main echo "This should only run for pushes to main"
[branch-filter-test/only-on-main] | This should only run for pushes to main
[branch-filter-test/only-on-main] ✅ Success - Main echo "This should only run for pushes to main" [66.615041ms]
[branch-filter-test/only-on-main] 🏁 Job succeeded
The job ran, even though the event explicitly describes a branch other than main. I repeated the exact same test with a paths: ["terraform/**"] filter instead of branches: —on a repository with not a single file inside terraform/— and the result was identical: the job ran anyway, with act not evaluating the path filter at all.
This is not a mistake in this lesson nor a misconfiguration: it's real, verified act behavior. branches:/paths: are filters GitHub applies server-side, even before assigning the job a runner — GitHub looks at the real event (which branch, which files changed) and decides whether it's even worth starting the workflow. act doesn't replicate that filtering: it takes the YAML, sees that the event type (push) matches something you declared in on, and runs the job — without looking at the detailed content of branches:/paths: to decide whether it should skip it.
The honest, practical consequence: branches: [main] on apply.yml (Module 5) remains the correct, necessary protection — it works exactly as you'd expect on real GitHub. What you can't do is use act to prove that filter works; act is going to lie to you on that specific point, running the job anyway. If you ever need to genuinely verify that a branch filter works, the only reliable way is testing it against a real GitHub repository — exactly the kind of gap Module 8 (lesson 5) names without sugarcoating it.
Going deeper: why apply.yml needs branches: [main] anyway
Even though act can't prove it, the filter remains indispensable in real production. Without branches: [main], an apply.yml listening for push would run on any branch —including a feature branch with half-finished changes— applying infrastructure nobody has reviewed yet. The filter turns "runs on any push" into "runs only when the change reached the branch that represents production's real state" — the piece that makes GitOps (Module 1, lesson 4) true: Git, and specifically main, is the source of truth, not just any work-in-progress branch.
Common mistakes
Trusting act push to "prove" a branch filter works (this lesson's central finding, now as a common mistake). What happens: someone writes branches: [main] on a workflow, tests it with act push -e other-branch-event.json, sees the job run, and mistakenly concludes the filter "doesn't work" or "something's misconfigured." Why it happens: it's reasonable to expect a tool that simulates GitHub Actions to replicate all of GitHub Actions' behavior. How to spot it: if your act test of a branch or path filter runs the job when you expected it to be skipped. How to fix it: remember this is one of act's documented limits —verified in this very lesson—: branches:/paths: filters are applied by GitHub's server, not by the engine running the job. The filter itself is written correctly; act simply doesn't evaluate it before starting.
Asking act for an event no job listens for (recap from Module 1, relevant again here). What happens: someone runs act release on anatomy-demo.yml, which only listens for push, pull_request, and workflow_dispatch. How to spot it: the Error: Could not find any stages to run message, already seen in Module 1. How to fix it: check act -l first — the Events column tells you, precisely, which events to ask for.
Exercises
Exercise 1 — Choose the right trigger for each scenario. For each situation, indicate whether you'd use push, pull_request, or workflow_dispatch: (a) run terraform plan when someone proposes an infrastructure change, before merging it; (b) run terraform apply when a change has already merged to main; (c) force a drift-detection job run right now, without waiting for its scheduled time.
See solution
(a) pull_request — you need evidence of the proposed change, before the possibility of merging it exists; it's exactly ci.yml's role in Module 3. (b) push (with branches: [main]) — apply should only run on code that already reached the real branch, never on a proposal; it's apply.yml's role in Module 5. (c) workflow_dispatch — it's the manual trigger, designed exactly for forcing a run outside its normal cycle; you're going to use it that way in Module 5, lesson 7.
Exercise 2 — Explain this lesson's finding to a skeptical colleague. A colleague tells you: "if act doesn't apply branches:, then act isn't good for anything, right?" Answer them in three sentences, without exaggerating in either direction.
See solution
A complete answer sounds, roughly, like this: "No, act is still extremely useful — it really executes the YAML, with the real Actions, inside Docker, and that covers the vast majority of what you need to verify before pushing a change. The only thing it doesn't replicate is a very specific filter —branches:/paths:— that GitHub applies centrally before assigning a runner; for that specific filter, you trust it's written correctly, and if you need to verify it with certainty, you test it against a real GitHub repository. It's not 'good for nothing,' it's 'doesn't replace 100% of what GitHub does, and this guide tells you exactly where.'"
Exercise 3 — Predict a combined filter's result. If apply.yml had on: push: branches: [main] paths: ["andes-cargo-infra/**"], and you ran act push with the default event (without specifying -e) from inside andes-cargo-infra/, would the job run? Why, based on what you learned in this lesson?
See solution
Yes, it would run — not because act's default event "satisfies" both filters correctly, but because, as you saw in this lesson, act doesn't evaluate either filter (branches: nor paths:) before deciding whether the job runs. As long as the event type (push) matches something declared in on, the job runs — regardless of what branch or paths the event describes, whether it's act's default or a hand-written one.
Summary and next step
In this lesson you ran an infrastructure pipeline's three most common triggers —push, pull_request, workflow_dispatch— and discovered, with a real, verified test, a concrete act limit: a push event's branches:/paths: filters aren't applied during simulation, because they're a GitHub server-side mechanism. That filter remains the correct protection for apply.yml in real production; what changes is that act, specifically, can't prove it to you.
Before moving on you should be able to: explain the difference between push and pull_request in one sentence; choose the right trigger for "review before merging" versus "apply after merging"; and explain, with technical precision, why act push runs a job even when the event describes a branch the branches: filter wouldn't allow on real GitHub.
Lesson 4 closes the infrastructure triggers trio with the fourth and last one: schedule, with its own cron syntax and its own simulation limitation.
Resources
- GitHub Docs — Events that trigger workflows — the complete official reference for
push,pull_request,workflow_dispatch, and every other event. - GitHub Docs — Triggering a workflow — official documentation for
branches:/paths:as filters applied by GitHub. - nektosact.com — User Guide — official documentation for how
actinterprets the event name passed on the command line.