GitHub Actions in December: workflow lockfiles and the end of open pull_request_target

Workflow lockfiles are GA and pull_request_target gets blocked in public repos on Dec 2. How to audit and lock down your CI before the deadline.

Igor Calabraro

· 7 min read

In this post

Almost every Next.js or Node project I come across runs tests, lint and deploys through GitHub Actions. And the pipeline is almost always treated as an afterthought: YAML copied from another repo, actions referenced by tag and, sometimes, a pull_request_target nobody remembers adding. Within a single week, GitHub changed both of those things. On October 28, workflow lockfiles became available on every plan. On November 2, the company announced that pull_request_target will be blocked by default in public repositories starting December 2. My take: CI is production code with access to your secrets, and now there is both a deadline and a tool to treat it that way.

#What changes and when

DateWhat happens
2026-10-28Workflow lockfiles GA (Free, Team, Enterprise Cloud)
2026-11-09, 16:00–18:00 UTCFirst brownout: pull_request_target temporarily blocked
2026-11-16, 16:00–18:00 UTCSecond brownout
2026-12-02Permanent block by default in public repositories

During those windows, affected workflows don't execute and show up as failed runs with an error annotation. Outside them, the rule stays in evaluate mode: workflows run, but with a warning.

Private and internal repositories are not affected, and neither is GitHub Enterprise Server. If your repo is public (an open source project, a template, a portfolio), this is on you.

#Why pull_request_target is dangerous

The difference between the triggers is context. pull_request runs the PR's code with reduced permissions, and a PR from a fork doesn't get the repository's secrets. pull_request_target, on the other hand, runs in the context of the base repository, with access to secrets, as GitHub's own announcement puts it. It was designed for tasks that don't need to execute the PR's code, such as labeling or commenting.

The trouble starts when someone combines that trigger with a checkout of the fork's code:

# DON'T do this: untrusted code running with secrets
on: pull_request_target
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test

A malicious postinstall in the fork's package.json, or a tampered test, now runs with a write token and your deploy secrets. This pattern became known as a "pwn request", and it's exactly what the block is meant to stop.

TriggerCode that runsSecrets on fork PRsIntended use
pull_requestThe PR'sNoBuild, lint, tests
pull_request_targetThe base repo'sYesLabeling and commenting, without running the PR
workflow_runThe base repo'sYesA trusted step after an unprivileged build

#Step 1: audit what you have

Start by listing where the trigger shows up:

grep -rn "pull_request_target" .github/workflows/

Before the first brownout, it's also worth opening Actions Insights, which GitHub points to for seeing which runs would be blocked. For each workflow you find, there are three ways out: drop the trigger, move to the pattern below or, if it's truly needed, allow it explicitly in an Actions event policy, which can target specific workflow files.

#Step 2: split the unprivileged build from the trusted step

The pattern I recommend for most cases is two workflows. The first runs on pull_request, with no secrets, and only produces an artifact:

# .github/workflows/pr-build.yml
name: PR build
on: pull_request
permissions:
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm check
      - run: echo "${{ github.event.pull_request.number }}" > pr-number.txt
      - uses: actions/upload-artifact@v4
        with:
          name: pr-meta
          path: pr-number.txt

The second fires when the first one finishes. It runs the base repository's code, so it can have write permissions, but it treats the artifact as untrusted input:

# .github/workflows/pr-comment.yml
name: Comment result
on:
  workflow_run:
    workflows: ["PR build"]
    types: [completed]
permissions:
  actions: read
  pull-requests: write
jobs:
  comment:
    if: github.event.workflow_run.event == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: pr-meta
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Comment on the PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CONCLUSION: ${{ github.event.workflow_run.conclusion }}
        run: |
          PR=$(tr -cd '0-9' < pr-number.txt)
          gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "PR build: $CONCLUSION"

Two details matter here. tr -cd '0-9' guarantees that only a number reaches the command, even if the fork tampered with the file. And event values go through environment variables instead of being interpolated straight into run, which prevents shell injection.

#Step 3: pin your actions with a lockfile

The second announcement targets a different vector: the tag that changes hands. When you write uses: some-org/some-action@v3, you're trusting that v3 will keep pointing at safe code. If the maintainer's account is compromised, the tag can be moved to another commit, and your pipeline runs the new version without any PR.

The lockfile fixes this by keeping the readable reference in the YAML while running the commit you locked. It's generated by GitHub's official CLI extension:

gh extension install github/gh-actions-lock

# from the repo root: every workflow
gh actions-lock

# or one workflow at a time
gh actions-lock .github/workflows/pr-build.yml

The extension writes .github/workflows/actions.lock, covering actions and their transitive dependencies, and rewrites uses: to the exact version it resolved. For example, @v3 may become @v3.1.0, pointing at the same commit. Local ./… references become $/…, which always resolve to the running commit.

For day-to-day work, three points:

  • Dependabot already understands the lockfile: when it opens a version bump for an action in a locked workflow, it updates the lockfile entry in the same PR. It doesn't onboard new workflows.
  • A tag that moved: gh actions-lock --relock re-resolves it. If the old commit is gone (force-push or deletion), you need --relock --accept-moved, and that deserves investigation before merging.
  • Review the lockfile diff like any dependency bump: a new commit is new code in your CI. A different owner_id or repo_id means the repository behind that name is no longer the same one.

Admins can also require lockfiles with the "Require lockfile" policy at the enterprise, organization or repository level, and check in CI that a lockfile is up to date without modifying any files.

#The same shift outside GitHub

This isn't an isolated move. On November 2, Vercel started blocking malicious npm package versions before and during installation, on every plan and with no configuration. If a project's lockfile points at a flagged version, the deploy fails and names the package. The direction is the same: the dependency chain, from package.json to the CI YAML, is now attack surface that gets the same scrutiny as application code.

#Conclusion

  • Audit pull_request_target before November 9. In public repos it stops running during the brownouts and for good on December 2.
  • Split the unprivileged build from the trusted step with pull_request + workflow_run, and treat any PR artifact as untrusted input.
  • Pin your actions with gh actions-lock and review the lockfile diff like any other dependency update.

#Sources

Want this level of care in your project?

Free 30-minute diagnosis: I look at your stack and tell you what needs attention.

Get my diagnosis

Liked it? Get the next one by email

One email when a new post is out. No spam — unsubscribe in one click.

By subscribing, you agree to the privacy policy.

Keep reading

All posts

Rather have me handle this in your project?

The same care as the posts, applied to your product. Free 30-minute diagnosis, no strings attached.