nyxcore-systems
5 min read

Unlocking Automated PRs: A Tale of Two GitHub Actions Permission Layers

Ever found your GitHub Actions workflow silently failing to create a pull request, despite seemingly correct permissions? I hit that wall recently while building my Vibe Publisher, and the solution involved navigating two distinct, often-confused permission layers.

GitHub ActionsCI/CDPermissionsAutomationWorkflowDevOpsTroubleshootingDeveloper Experience

As developers, we often build tools to make our lives easier. For me, one such tool is the Vibe Publisher – a custom GitHub Actions workflow designed to automate the creation of blog posts (like this one!) directly from my development session notes, which I call "session letters." It's a form of dogfooding, where the tool I'm building helps me document the process of building it.

The idea is simple: I jot down my daily progress, learnings, and pain points in a structured markdown file (.memory/YYYY-MM-DD-session.md). The Vibe Publisher then picks up these "letters," processes them, and drafts a public-ready blog post, opening a pull request for review. It's an awesome feedback loop, turning raw session memory into shareable knowledge.

But, as with any automation involving code changes, permissions are always the gatekeeper. And recently, the Vibe Publisher hit a very stubborn gate.

The Goal: From Session Note to Pull Request

My immediate objective was clear: Get the Vibe Publisher workflow to successfully create new branches and open pull requests. It needed to:

  1. Read the session letter.
  2. Generate the blog post content.
  3. Commit the new content to a new branch.
  4. Open a pull request for that branch.

The first two steps were working fine. The issue arose at step 3 and 4: the workflow was failing with a cryptic message about not being permitted to create branches or PRs.

First Attempt: Workflow-Level Permissions

My initial thought, and the most common solution for GitHub Actions permissions, was to add a permissions block directly to my vibe_publisher.yml workflow file. This block controls the permissions granted to the GITHUB_TOKEN for that specific workflow run.

Based on the documentation and common patterns for actions like peter-evans/create-pull-request@v6 (which I was using to create the PR), I added:

yaml
# .github/workflows/vibe_publisher.yml
name: Vibe Publisher

on:
  push:
    branches:
      - main
    paths:
      - '.memory/**'

jobs:
  publish-vibe:
    runs-on: ubuntu-latest
    permissions: # <-- My first attempt, added here
      contents: write
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      # ... other steps to process memory and generate blog post ...

      - name: Create Pull Request
        id: cpr
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: "feat: new blog post from session memory"
          # ... other PR creation options ...

I ran the workflow, full of optimism. Green checkmark? Nope. Still failing. The error message was something along the lines of: "GitHub Actions is not permitted to create or approve pull requests."

Frustrating, right? I had explicitly given pull-requests: write and contents: write. What was I missing?

The "Aha!" Moment: Two Layers of Permissions

This is where the real lesson came in. It turns out there are two distinct layers of permissions that need to be configured for GitHub Actions to create pull requests:

  1. Workflow-level permissions: Configured in your .github/workflows/*.yml file using the permissions block. This dictates the scope of the GITHUB_TOKEN for that specific workflow run.
  2. Repository-level permissions: A global setting for your repository that dictates whether GitHub Actions, in general, is allowed to create pull requests at all.

My workflow-level permissions were correct, but the repository-level setting was silently blocking the operation. This setting is often overlooked because it's not in your code; it's deep in the repository settings.

To fix this, I needed to update the repository's default permissions for GitHub Actions. You can do this via the GitHub UI (Settings > Actions > General > Workflow permissions) or, more programmatically (and what I did), using the gh api command-line tool.

I ran the following to update my repository's settings:

bash
# Update default workflow permissions to 'write' and allow PR creation
gh api \
  --method PUT \
  -H "Accept: application/vnd.github.v3+json" \
  /repos/{owner}/{repo}/actions/permissions/workflow \
  -f default_workflow_permissions='write' \
  -f can_approve_pull_request_reviews=true

Let's break down those parameters:

  • default_workflow_permissions: write: This sets the default permission for all workflows to write. While my specific workflow had permissions: write, setting this globally ensures any new workflows or workflows without an explicit permissions block also have sufficient access.
  • can_approve_pull_request_reviews: true: This was the crucial one. This specific flag explicitly grants GitHub Actions the ability to create pull requests. Even with pull-requests: write in the workflow, if this repo-level setting is false, it's a no-go.

The Fix: Both Layers Enabled

With both the workflow-level permissions block in vibe_publisher.yml and the repository-level settings updated, I triggered the Vibe Publisher workflow again.

Success! The workflow passed green, a new branch was created, and a pull request (for this very blog post, ironically) popped up for review. All four of my core CI/CD workflows are now fully operational, including linting, unit tests, E2E tests, and now the Vibe Publisher.

Lessons Learned

  1. Two Layers of Permissions: Always remember that GitHub Actions permissions for PR creation operate on two distinct levels:
    • Workflow-level: In your .yml file, specifying contents: write and pull-requests: write.
    • Repository-level: In Settings > Actions > General, ensuring "Read and write permissions" is set and "Allow GitHub Actions to create and approve pull requests" is enabled.
  2. Specific Error Messages: The error message "GitHub Actions is not permitted to create or approve pull requests" is a strong indicator that you need to check the repository-level settings, not just your workflow file.
  3. gh api for Automation: For programmatic control over repository settings, the gh api command-line tool is incredibly powerful and useful.

This small but critical fix unblocks a significant piece of my development workflow automation. It's a reminder that even seemingly straightforward tasks can hide layers of configuration, and sometimes, the most effective debugging is knowing where to look for those layers.

Now, if you'll excuse me, I have a pull request to review... for this blog post!


json
{"thingsDone":["Fixed Vibe Publisher GitHub Actions workflow to create branches and PRs","Added workflow-level permissions (contents: write, pull-requests: write)","Updated repo-level GitHub Actions permissions via gh api (default_workflow_permissions: write, can_approve_pull_request_reviews: true)","All 4 CI/CD workflows now operational (CI, Vibe Publisher)","Enriched project list page with per-card stats","Fixed 3 previous CI pipeline jobs (lint, kimi test, pgvector E2E)","Cleaned up ~40 lint errors"],"pains":["Workflow still failed after adding only workflow-level permissions","Encountered 'GitHub Actions is not permitted to create or approve pull requests' error","Discovered the need for separate repo-level permission setting"],"successes":["Successfully enabled automated PR creation","Achieved full CI/CD workflow operationality","Learned about the dual-layer GitHub Actions permissions model"],"techStack":["GitHub Actions","YAML","Bash","gh cli","peter-evans/create-pull-request@v6"]}