A GitHub Action that verifies if the safe to test label is assigned to a Pull Request before running sensitive steps. By default, every new workflow run requires a new assignment of the label to prevent an attacker from pushing malicious code after you marked the code as safe with the label.
If you are using pull_request_target in your workflows, there is a high probability your repository is vulnerable to secret exfiltration. This action acts as a manual "Gatekeeper" to protect your infrastructure.
- Add the
labeledtype to yourpull_request_targettrigger. - Add
nilsreichardt/verify-safe-to-test-label@a65b1d76a873fe060b8ece697f238ef8805e6e23to the start of your job.
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
# Used to trigger the action when the "safe to test" label is added to the PR
- labeled
jobs:
integration-tests:
runs-on: ubuntu-latest
permissions:
contents: read # Recommended for actions/checkout action, see https://github.com/actions/checkout?tab=readme-ov-file#recommended-permissions
pull-requests: write # Required when require-reapproval=true
steps:
# 1. Check the gate (and reset it on every workflow run when require-reapproval=true)
# If the PR is not from a fork, the action will always pass and code is considered as safe to execute.
- name: Ensure PR has "safe to test" label, if PR is from a fork
uses: nilsreichardt/verify-safe-to-test-label@a65b1d76a873fe060b8ece697f238ef8805e6e23
with:
label: "safe to test" # optional, default is "safe to test"
require-reapproval: true # optional, default is true.
# 2. Securely run your tests
- name: Checkout PR code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Test with Secrets
run: npm install && npm test
env:
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}If you have multiple jobs in your workflow, you can have a job that verifies the label and other jobs require this job to pass.
# [...]
jobs:
verify-safe-to-test-label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Ensure PR has "safe to test" label, if PR is from a fork
uses: nilsreichardt/verify-safe-to-test-label@a65b1d76a873fe060b8ece697f238ef8805e6e23
macos-tests:
needs: verify-safe-to-test-label # This job will only run if the verify-safe-to-test-label job passes
runs-on: macos-latest
steps:
# [... steps for macos tests]
linux-tests:
needs: verify-safe-to-test-label # This job will only run if the verify-safe-to-test-label job passes
runs-on: ubuntu-latest
steps:
# [... steps for linux tests]| Name | Description | Default |
|---|---|---|
label |
The name of the label required to pass the check. Always passes if the pull request is not from a fork. | safe to test |
require-reapproval |
Remove the label on every workflow run to force re-review of new commits. When you set this to false, an attacker could push malicious code after you marked the code as safe with the label. |
true |
repo-token |
Token used to remove labels when require-reapproval=true. Requires pull-requests: write |
github.token |
pull_request_target runs in the context of the base repository, so it can access the base repo's GITHUB_TOKEN and any secrets you expose to the job. If you checkout + execute fork code, you’ve created a trust boundary violation. An attacker could steal secrets and push malicious commits to your repository.
Caution
Is your CI/CD pipeline insecure? You are at risk if all three are true:
- You use the
pull_request_targettrigger. - You check out code from the head (the fork).
- You execute scripts from that code (e.g.,
npm install,npm test,make,python setup.py).
A note on legacy repositories: If your repository was created before February 2023, your GITHUB_TOKEN likely has write-permissions by default. An attacker could not only steal secrets but also push malicious commits directly to your main branch. You can verify this setting as follows: Settings > Actions > General > Workflow permissions.
The safest path is using the standard pull_request trigger, but this hides secrets from forks, often breaking legitimate integration tests.
The Label Gate solution allows you to keep pull_request_target while adding a human-in-the-loop:
- A contributor submits a PR.
- The CI runs but fails immediately at the
verifystep. - A maintainer reviews the code. If it's safe, they add the
safe to testlabel. - After the label assignment, the workflow is re-triggered. Now the action passes, and secrets are exposed only to the code you've vetted.
- By default, after the workflow run, the label is removed again to require a new assignment for the next workflow run.
The action requires the smallest possible permissions:
| Permission | Why |
|---|---|
pull-requests: write |
Required only to remove the label when require-reapproval=true |
No other permissions are used or needed.
This action is intended to run in a high-privilege CI context (pull_request_target).
For that reason, the design goal is to make the behavior easy to understand and audit.
The action only:
- Reads pull request metadata (labels)
- Fails the workflow if the required label is missing
- Optionally removes the label via the GitHub REST API
The action:
- does not check out repository code
- does not execute shell commands
- does not read repository files
- does not access workflow secrets
- does not send network requests to external services
- does not include telemetry, analytics, or tracking
The only network communication is to api.github.com using the official GitHub Octokit client.
The action intentionally uses only official GitHub libraries:
@actions/core@actions/github
No third-party runtime dependencies are included.
The source code is intentionally small and readable so it can be reviewed quickly. Maintainers are encouraged to audit the action before use. The full behavior is contained in a single file: index.js
Because the action performs only label inspection and a single GitHub API call, a manual review typically takes only a few minutes.
The behavior is fully unit-tested, and tests verify the label validation and permission handling logic.
For maximum supply-chain security, you should pin this action to a full commit SHA instead of a version tag:
uses: nilsreichardt/verify-safe-to-test-label@<full-commit-sha>A commit SHA is immutable and protects you if a future release is compromised.
See GitHub’s guidance: Security hardening for GitHub Actions
If you find a security vulnerability for this action, please report it privately via GitHub’s "Report a vulnerability" feature. See SECURITY.md for details.