GitHub Foundations study materials
GitHub Foundations study resources

Twenty-two minutes per PR and a deploy token that is not there

GitHub Foundations · Put your knowledge to work in context.

All case studies

Twenty-two minutes per PR and a deploy token that is not there

A maintainer's public repository runs a GitHub Actions workflow on every pull request: a three-version Node matrix, a full dependency install, the test suite, and a deploy step that uses the DEPLOY_TOKEN secret. Total runtime is 22 minutes and the organization's included minutes are nearly exhausted. A first-time contributor opens a pull request from a fork; the two test jobs pass but the deploy job fails with an empty token. The maintainer considers switching the trigger to pull_request_target so secrets are available, and separately considers adding actions/cache in front of the install step.

1. Why is DEPLOY_TOKEN empty for the fork pull request, and why is pull_request_target a dangerous fix?

Read the worked answer

Secrets are not passed to workflows triggered by pull_request from a fork. The job runs with a read-only GITHUB_TOKEN and no repository secrets. This is intentional: otherwise any stranger could open a pull request that prints your credentials.

pull_request_target does receive secrets because it runs in the base repository's context, and that is precisely the hazard. If the workflow checks out the fork's head and executes it, the contributor controls the code that secrets are exposed to, and can exfiltrate them. That is a well-known escalation pattern, not a theoretical one.

Safe patterns:

  • Keep the tests on pull_request and gate deployment with if: github.event.pull_request.head.repo.full_name == github.repository, so fork PRs simply never run deploy.
  • Deploy from a workflow that runs on push to the default branch after merge, protected by an environment with required reviewers.
  • Never combine pull_request_target with a checkout of untrusted head code; use it only to comment or label.

For this contributor, the tests already passed; merging is the deploy trigger.

2. The maintainer wants to cut the 22 minutes and the minute spend. Which changes actually help?

Read the worked answer

Separate wall-clock time from billable minutes, because they respond to different changes.

Billable minutes: a three-version matrix runs in parallel, so it costs three times the minutes of one job while taking roughly as long as the slowest. Dropping to two versions, or to one plus a scheduled nightly full matrix, cuts spend immediately. Add a concurrency group with cancel-in-progress: true so a push that supersedes an in-flight run kills it rather than paying for both. Add path filters so documentation-only changes do not run the suite at all.

Wall clock: caching speeds the install, not the tests. Use actions/cache or the cache input on the setup action, keyed on hashFiles('**/package-lock.json'). Note the scoping rules: caches are branch-scoped, a pull request can restore from the base branch cache but cannot write to it, so the first PR after a dependency change is always cold. Cache the package manager's download directory, not node_modules.

Measure first. If install is 2 of 22 minutes, caching wins little and the matrix change wins everything.

3. The maintainer proposes moving the deploy step into the same workflow but behind an environment. Is that sufficient?

Read the worked answer

It is an improvement but not a complete answer, and the difference matters.

An environment with required reviewers means a job cannot start until a human approves it. Combined with environment-scoped secrets, that stops the deploy job from running on an unreviewed fork pull request, and the reviewers see a pending deployment in the UI. That addresses the empty-token symptom and the exfiltration risk at once.

What it does not fix:

  • The workflow definition itself still comes from the pull request, so a reviewer must read the diff of the workflow file, not just click approve. Pin actions to full commit SHAs, not tags, for anything in the deploy path.
  • The secret must be scoped to the environment rather than the repository, or any job can read it.
  • Deployment still happens on a pull request rather than on a merge, which means untested-together commits later. Deploying from the default branch after merge is simpler to reason about.

Environment approval is a good gate; it is not a substitute for deploying the merged artifact.