2.1 Webhook & Code Events: push, pull_request, and pull_request_target
Key Takeaways
- The push event triggers on Git ref updates (branches, tags) and can be scoped using branches, tags, and paths filters.
- The pull_request event triggers by default only on opened, synchronize, and reopened activity types; other activity types like closed, labeled, or ready_for_review require explicit configuration.
- Workflows triggered by pull_request execute against the ephemeral merge commit ref refs/pull/NUMBER/merge, running in the fork's context with a read-only GITHUB_TOKEN and zero access to base repository secrets.
- The pull_request_target event executes in the context of the base repository (target branch) with access to repository secrets and write permissions, evaluating the workflow file from the base branch rather than the PR head branch.
- Combining pull_request_target with an explicit checkout of untrusted PR code (ref: ${{ github.event.pull_request.head.sha }}) introduces severe security vulnerabilities (pwn-requests) if untrusted build scripts or actions are executed.
Webhook & Code Events: push, pull_request, and pull_request_target
Every automated pipeline in GitHub Actions begins with an event trigger. GitHub Actions is event-driven by design: an event is a specific activity that triggers a workflow run. In software development lifecycles, the most frequent triggers are code lifecycle events—specifically when commits are pushed directly to a repository branch or submitted through a pull request.
Understanding the subtle execution contexts, Git reference structures, and security boundaries between push, pull_request, and pull_request_target is one of the highest-weight competency areas in the GitHub Actions Certification (GH-200) examination.
1. Deep Mechanics of the push Event
The push event occurs whenever one or more commits are pushed to a repository branch or when a Git tag is pushed. It represents direct code changes landing in the repository.
on:
push:
branches:
- main
- 'releases/**'
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
paths:
- 'src/**'
- 'package.json'
Key Characteristics and Context Values
- Triggering Ref (
github.ref): For branch pushes,github.refis formatted asrefs/heads/<branch_name>(e.g.,refs/heads/main). For tag pushes, it resolves torefs/tags/<tag_name>(e.g.,refs/tags/v1.0.0). - Target Commit SHA (
github.sha): For branch pushes,github.shais the commit SHA of the tip of the pushed branch. For tag pushes, it is the commit SHA to which the tag points. - Commit Comparison Payload: The event payload provides
github.event.before(the commit SHA prior to the push) andgithub.event.after(the commit SHA after the push), allowing workflows to compute precise diff ranges. - Default Checkout Behavior: When using
actions/checkout@v4, the action automatically checks outgithub.refatgithub.sha, representing the exact state of the pushed branch or tag.
2. The pull_request Event Architecture
The pull_request event triggers on activity related to pull requests. Unlike push, a pull request represents proposed changes from a head branch (which may reside in the same repository or in an external fork) against a base branch.
Default Activity Types vs. Explicit Types
By default, if you specify on: pull_request without defining types, GitHub Actions triggers the workflow for only three activity types:
opened: A new pull request is created.synchronize: New commits are pushed to the head branch of the pull request.reopened: A previously closed pull request is reopened.
If you need a workflow to respond to other pull request lifecycle events, you must explicitly declare all desired activity types in the types array. Specifying types overrides the defaults completely:
| Activity Type | Description | Common CI/CD Use Case |
|---|---|---|
opened | Pull request opened | Initial lint, unit tests, security scanning |
synchronize | New commit pushed to PR branch | Re-running test suites on updated code |
reopened | Closed PR reopened | Re-validating baseline checks |
closed | PR merged or closed without merging | Triggering post-merge cleanup, closing test environments |
labeled / unlabeled | Label added or removed | Automated routing, triage, triggering stage deployments |
ready_for_review | Draft PR marked ready for review | Running resource-heavy integration test suites |
converted_to_draft | PR converted back to draft status | Pausing continuous review automations |
assigned / unassigned | Reviewer or assignee updated | Team notification webhooks |
edited | PR title or description edited | Enforcing Conventional Commits / PR title linters |
[!IMPORTANT] When handling the
closedactivity type, always inspect the boolean context variable${{ github.event.pull_request.merged }}. Ifgithub.event.pull_request.merged == true, the PR was merged into the base branch; iffalse, the PR was closed without being merged.
on:
pull_request:
types: [opened, synchronize, reopened, closed, labeled]
branches:
- main
jobs:
deploy-cleanup:
if: github.event.action == 'closed' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- run: echo "PR #${{ github.event.pull_request.number }} merged into main! Cleaning up preview env..."
Ref Resolution: The Ephemeral Merge Commit
When a pull_request event triggers, GitHub creates a synthetic, ephemeral test merge commit.
- Merge Ref:
github.refpoints torefs/pull/<pr_number>/merge. - Head Ref:
refs/pull/<pr_number>/headpoints to the tip of the PR branch.
When actions/checkout@v4 runs in a pull_request workflow, it defaults to checking out refs/pull/<pr_number>/merge. This ensures that your CI pipeline tests the proposed changes as if they were already merged into the current base branch, catching integration regressions and merge conflicts before the pull request is approved.
[!WARNING] If merge conflicts exist between the PR head branch and the base branch, GitHub cannot generate the synthetic
refs/pull/<pr_number>/mergeref. In such cases, the workflow run will fail at checkout or may fail to trigger until conflicts are resolved.
3. The Security Boundary: pull_request vs pull_request_target
In enterprise and open-source workflows, managing pull requests from external forks requires a strict balance between automation convenience and security isolation.
The pull_request Fork Security Model
When a workflow is triggered by pull_request from a forked repository:
- Workflow Definition Source: The workflow file is read from the head branch (the fork).
- GITHUB_TOKEN Permissions: The automatic token is scoped to read-only (
contents: read), regardless of repository default settings. - Secrets Access: The workflow has zero access to repository secrets.
${{ secrets.* }}resolves to empty strings. - Runner Security: Protects the upstream repository from malicious code execution and credential exfiltration.
However, this isolation creates challenges for legitimate automations that need to label PRs, post bot comments, or execute SonarQube / external service analysis requiring API keys.
The pull_request_target Event
To solve the credential isolation problem, GitHub introduced pull_request_target. This event triggers on pull request activities but alters the execution environment fundamentally:
- Workflow Definition Source: The workflow file is loaded strictly from the base repository target branch (e.g.,
main). Changes made to.github/workflows/inside the PR are completely ignored. - Execution Context:
github.refresolves to the base branch (e.g.,refs/heads/main). - Token & Secrets: The workflow receives the base repository GITHUB_TOKEN (with standard write permissions) and has full access to repository and organization secrets.
The "Pwn-Request" Attack Vector and Safe Usage Rules
Because pull_request_target runs with write permissions and secret access, checking out untrusted code from the fork PR head branch is extremely dangerous.
# ❌ INSECURE ANTI-PATTERN: PWN-REQUEST VULNERABILITY
on:
pull_request_target:
jobs:
unsafe-build:
runs-on: ubuntu-latest
steps:
- name: Checkout untrusted fork code into privileged runner
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # DANGER!
- name: Run build script
run: |
npm install
npm test # Attacker injects malicious postinstall script to steal secrets!
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
AWS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
[!CAUTION] Pwn-Request Exploitation: If you checkout the PR head commit (
github.event.pull_request.head.sha) in apull_request_targetworkflow and execute any build step, test command, linter, or action from that checked-out code, an external attacker can submit a pull request containing malicious scripts (e.g., insidepackage.jsonscripts or custom code) that execute inside your privileged environment and exfiltrate all secrets.
Recommended Safe Pattern for pull_request_target
Only use pull_request_target for lightweight administrative tasks (such as labeling, checking PR metadata, or posting comments) where untrusted code is never checked out or executed:
# ✅ SECURE PATTERN: Metadata handling without checking out untrusted code
name: Label Pull Request
on:
pull_request_target:
types: [opened, labeled]
permissions:
pull-requests: write
contents: read
jobs:
auto-label:
runs-on: ubuntu-latest
steps:
- name: Add Triage Label
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['needs-review']
});
| Feature | pull_request | pull_request_target |
|---|---|---|
| Workflow Code Source | PR Head / Fork Branch | Base Branch (main) |
| Checked-out Ref (Default) | refs/pull/:id/merge | refs/heads/<base> |
| Fork Secrets Access | None (Disabled) | Full Target Repo Secrets |
| GITHUB_TOKEN Scope | Read-Only (on forks) | Read/Write (configurable) |
| Primary Use Case | Building & Testing Code | PR Labeling, Triage, Commenting |
An engineer needs a workflow to run automated linting and tests on every commit pushed to a pull request, but also wants to execute a cleanup job whenever the pull request is merged into the default branch. How should the pull_request event be configured in the workflow?
A public open-source repository maintains a workflow triggered by pull_request_target to run CI tests. The workflow checks out the code using actions/checkout@v4 with ref: ${{ github.event.pull_request.head.sha }} and executes npm test. What critical security vulnerability does this configuration introduce?
What Git reference does GitHub Actions check out by default when using actions/checkout@v4 in a workflow triggered by the pull_request event?