GitHub Actions Fundamentals
Topic 49

Events and Triggers

Actions

The on: key decides what wakes a workflow up. A push, a pull_request, a manual workflow_dispatch, a cron schedule, or any of dozens of other repo webhooks can be a trigger, and most workflows subscribe to two or three of them. Choosing the right events — and scoping them with filters — is most of what separates a pipeline that runs when it should from one that burns minutes on every irrelevant change.

One trigger deserves its own warning label before anything else: pull_request_target. It runs with the base repository's secrets and a write token, against a context derived from a fork's pull request. Used correctly it automates labels and comments; used carelessly it hands a stranger your credentials. The rest of this page builds up to why.

Push and Pull Request

The two everyday triggers are push and pull_request. Both take branch, tag, and path filters so you can fire only on the branches and files that matter. pull_request also has activity types — opened, synchronize (a new commit pushed to the PR), and reopened are the ones that mean "there is new code to test." Listing them explicitly skips redundant runs on label or assignee changes.

Run CI on PRs to main, and on pushes to main
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

Manual and Scheduled

workflow_dispatch adds a "Run workflow" button in the Actions UI and accepts typed inputs so a manual run can be parameterized and validated. Note that the button only appears once the workflow file exists on the default branch — a dispatch trigger on a feature branch alone is invisible. schedule runs on POSIX cron, always in UTC, with a minimum interval of five minutes, and only ever on the default branch.

A nightly job and a manual trigger with an input
on:
  schedule:
    - cron: '0 3 * * *'
  workflow_dispatch:
    inputs:
      environment:
        type: choice
        options: [staging, production]
        required: true

The pull_request_target Hazard

For a pull request from a fork, the safe default pull_request runs against the PR's merge commit with a read-only token and no access to your secrets — precisely because the code came from someone you do not control. pull_request_target inverts that: it runs in the context of the base branch, with full secrets and a write GITHUB_TOKEN. That is exactly what you want for labeling or commenting on a PR, and exactly what you must never combine with checking out and executing the fork's code.

Filtering

Filters narrow which activity actually triggers a run: branches/branches-ignore, paths/paths-ignore, and tags. You cannot use the include and ignore form of the same filter together under one event — branches alongside branches-ignore is a configuration error. A paths: filter is the standard way to skip CI when only documentation changed.

Other Triggers

Beyond push and PR there is a long tail: release for publishing flows, issues for triage automation, workflow_run to chain one workflow off another's completion, and repository_dispatch for kicking a workflow from an external system over the API. They all plug into the same on: key and the same event-driven model.

pull_request vs pull_request_target

pull_request (safe default) — runs against the PR's merge commit with a read-only token and no secrets for fork PRs. This is the trigger for building and testing contributor code, because even malicious code in the PR cannot reach your credentials.

pull_request_target — runs against the base ref with full secrets and a write token. Use it only for automation that comments on or labels a PR and never checks out or executes the PR's code. Checking out the fork's head here and running it is a textbook credential-theft vector.

Common Mistakes
  • Using pull_request_target and then checking out the PR's head ref with actions/checkout — you just ran a fork's code with your secrets and write token, the classic credential-theft vector.
  • Expecting schedule cron to fire exactly on time, when runs queue and can be delayed 15+ minutes under load and only run on the default branch.
  • Writing cron in local time — schedule is always interpreted as UTC, so the job fires hours off from what you expected.
  • Combining branches and branches-ignore under the same event, which errors instead of merging the two filter forms.
  • Adding workflow_dispatch on a feature branch and wondering why the "Run workflow" button never appears — it shows only once the file is on the default branch.
Best Practices
  • Default to pull_request; reach for pull_request_target only for label/comment automation that never executes PR code.
  • Define workflow_dispatch.inputs with type: and required: so manual runs are validated before they start.
  • Scope pull_request with types: [opened, synchronize, reopened] to skip redundant runs on label and assignee changes.
  • Use paths: filters to skip CI when only docs or unrelated directories change.
  • Write all cron expressions in UTC and pad in slack, since scheduled runs can be delayed under load.
Comparable toolsGitLab CI/CD workflow:rules, only/except, pipeline schedulesCircleCI when and scheduled pipelinesJenkins triggers and cronAzure Pipelines trigger / pr / schedules

Knowledge Check

Why is checking out a fork's code under pull_request_target dangerous?

  • It runs with the base repo's secrets and write token, so fork code can steal your credentials
  • It cannot check out fork code at all, so the checkout step errors and fails the run
  • It runs with a read-only token and no secrets exposed, so the fork code just wastes runner minutes for nothing
  • Checking out the fork head deletes the base branch's working tree on the runner

How do token permissions differ for a fork PR between the two events?

  • pull_request is read-only with no secrets; pull_request_target gets a write token and secrets
  • Both get a write token and full secrets, since the PR targets your repository
  • Both get a read-only token and no secrets, because the code is from a fork
  • pull_request gets the write token and full secrets; pull_request_target is the read-only, sandboxed one

A cron schedule trigger lives only on a feature branch. When does it run?

  • Never — scheduled runs fire only from the default branch
  • On the feature branch at the scheduled cron time, like any other branch
  • On every branch that contains a copy of the workflow file
  • Only after merge, with a retroactive catch-up run for the missed slot

What happens if you set both branches and branches-ignore under the same event?

  • It is a configuration error — you cannot mix the two forms of one filter
  • GitHub merges the two lists and drops any branch that appears in both
  • The branches-ignore list silently wins and the include list is dropped
  • Both apply at once, so the intersection is empty and nothing matches

You got correct