Skip to main content

Fixing GitHub Actions "Resource not accessible by integration" in Protected Branches

 If you are maintaining a repository with strict security defaults or branch protection rules, you have likely encountered this error log during a release pipeline or a PR automation step:

HttpError: Resource not accessible by integration
    at /home/runner/work/.../index.js:14:10
    ...
    status: 403

This 403 Forbidden error is the standard response when the ephemeral GITHUB_TOKEN attempts a write operation (creating a release, tagging a commit, or commenting on a PR) but lacks the specific OAuth scope required to execute it.

The Root Cause: Least Privilege Defaults

Historically, the auto-generated GITHUB_TOKEN provided to workflows had read and write access to almost all scopes by default. This was convenient but presented a massive surface area for supply chain attacks. If a third-party action was compromised, it could wipe your repository.

GitHub updated the default setting for new organizations and repositories to Restricted (Read-only). Furthermore, many DevOps teams now enforce this restriction at the organization level (Settings -> Actions -> General -> Workflow permissions).

When your pipeline attempts to:

  1. Run semantic-release (requires creating Git tags and Releases).
  2. Run a linter that comments on a PR (requires accessing the Issues/PR API).

It fails because the token generated for that run has only contents: read permissions.

The Fix: Explicit Workflow Permissions

Do not change the repository-wide default back to "Read and write." That defeats the security model. Instead, you must explicitly declare the required scopes within the workflow YAML using the permissions key.

Scenario 1: Automated Releases (Tagging and Releasing)

For pipelines that need to push tags back to the repository and create GitHub Releases, you must grant contents: write.

File: .github/workflows/release.yml

name: Production Release

on:
  push:
    branches:
      - main

# 1. GLOBAL PERMISSIONS
# Explicitly set top-level permissions. 
# We default to 'read-all' to ensure least privilege for non-specified scopes.
permissions: read-all

jobs:
  semantic-release:
    runs-on: ubuntu-latest
    
    # 2. JOB-LEVEL PERMISSIONS (Recommended)
    # Overwrite the global permissions for this specific job.
    # 'contents: write' allows creating tags and releases.
    # 'issues: write' and 'pull-requests: write' are required if the 
    # release bot needs to comment on issues/PRs indicating a release happened.
    permissions:
      contents: write
      issues: write
      pull-requests: write

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Required for history analysis
          persist-credentials: false # We will use the GITHUB_TOKEN explicitly

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Create Release
        env:
          # The token is automatically available, but usually needs 
          # to be passed explicitly to tools like semantic-release
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npx semantic-release

Scenario 2: PR Comments (Linters and Coverage)

If your workflow runs on pull_request and needs to comment on that PR (e.g., adding a code coverage summary), contents: write is not enough. You need pull-requests: write.

File: .github/workflows/pr-check.yml

name: PR Quality Check

on:
  pull_request:
    branches: [main]

permissions: read-all

jobs:
  test-and-comment:
    runs-on: ubuntu-latest
    permissions:
      # Required to read code
      contents: read
      # Required to post comments on the PR
      pull-requests: write
      # Required if you want to update checks/statuses on the commit
      checks: write

    steps:
      - uses: actions/checkout@v4
      
      - name: Run Tests
        run: npm test -- --coverage
        
      - name: Comment Coverage
        uses: thollander/actions-comment-pull-request@v2
        with:
          message: |
            ### Test Coverage Report
            Tests passed successfully!
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Explanation

1. The permissions Key

The permissions key modifies the JWT (JSON Web Token) claims associated with the GITHUB_TOKEN. When the workflow starts, GitHubMint checks this YAML definition against the repository's maximum allowed policy. If valid, it provisions the token with exactly those scopes.

2. Specific Scopes

  • contents: write: Grants access to git push (tags/commits) and the Releases API.
  • pull-requests: write: Grants access to the Pull Request API (comments, reviews, labels).
  • issues: write: Grants access to the Issues API. Note that in GitHub's internal architecture, PRs are technically issues, so sometimes issues: write covers PR comments, but pull-requests: write is more explicit and preferred for PR interactions.

3. Protected Branches Nuance

If you apply the code above and still receive an error specifically when pushing to main, check your Branch Protection Rules (Settings -> Branches -> 'main').

If "Restrict who can push to matching branches" is enabled, the GITHUB_TOKEN (acting as the github-actions[bot] user) will be blocked even with contents: write.

Solution for Protected Branches:

  1. Go to the Branch Protection rule for main.
  2. In the "Restrict who can push" section, search for and add github-actions[bot] (or the specific app name if using a GitHub App).
  3. Alternatively, if you require bypassing pull request reviews for releases, you must use a Personal Access Token (PAT) belonging to an admin or a GitHub App token, as the default GITHUB_TOKEN cannot bypass branch protection rules regardless of permissions.

Conclusion

The Resource not accessible by integration error is a feature, not a bug. It enforces the principle of least privilege. By explicitly defining permissions in your YAML, you adhere to modern security practices while ensuring your CI/CD pipelines have exactly the access they need—no more, no less.