nyxcore-systems
5 min read

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

We aimed to automate branch and pull request creation via GitHub Actions for our internal session notes. What seemed like a straightforward task quickly became a deep dive into GitHub's nuanced permission system, revealing two distinct layers that both needed attention. Here's how we conquered it.

GitHub ActionsCI/CDPermissionsAutomationWorkflowDevOpsDeveloper Experience

As developers, we're constantly looking for ways to streamline our workflows and enhance our development experience. One of our recent initiatives involves a cool internal tool we call the "Vibe Publisher." Its mission? To transform our raw development session memories (saved as .memory/ files) into structured branches and pull requests, making our progress transparent and easily reviewable. It's about turning transient notes into persistent, actionable records.

Sounds great, right? Automate the mundane, focus on the code. But, as with many automation dreams, we hit a snag. A permissions snag, to be precise.

The Goal: Automating Our Session Handoffs

Our vibe_publisher.yml GitHub Actions workflow is designed to detect new .memory files, create a new branch, commit the processed content, and then open a pull request. This allows us to capture insights, action points, and decisions from our dev sessions directly into our codebase, ready for review and integration. It's a fantastic way to maintain a living, breathing project documentation.

The core of this automation relies on the excellent peter-evans/create-pull-request@v6 action, which handles the branch creation and PR opening magic. All we needed was for our GitHub Action to have the necessary permissions. Simple, right?

The First Hurdle: Workflow-Level Permissions

Our initial thought was to grant the workflow specific permissions directly within its YAML file. GitHub Actions provides a permissions block where you can scope the GITHUB_TOKEN for the workflow run. We figured contents: write (to push branches and commits) and pull-requests: write (to create PRs) would be sufficient.

Our vibe_publisher.yml looked something like this (simplified):

yaml
name: Vibe Publisher

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

permissions:
  contents: write    # Allow the workflow to push commits and create branches
  pull-requests: write # Allow the workflow to create pull requests

jobs:
  publish_vibe:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      # ... (logic to process .memory files) ...

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: "feat: Vibe Publisher update from session memory"
          branch: "vibe-publisher/{{ ref }}" # Dynamic branch name
          delete-branch: true
          title: "Vibe Publisher: New Session Memory"
          body: "This PR contains updates generated by the Vibe Publisher from a recent development session."
          labels: "documentation, automation"

We pushed the changes, triggered the workflow, and... it failed. The error message was clear, yet frustratingly vague: "GitHub Actions is not permitted to create or approve pull requests."

Wait, what? We just added pull-requests: write! What gives?

Lessons Learned: The Two Layers of GitHub Permissions

This is where the "pain log" turned into a critical learning moment. The error message pointed to a deeper, repository-level permission setting that overrides or complements the workflow-level token scopes.

It turns out, GitHub Actions has two distinct layers of permissions that must both be correctly configured for certain operations, especially those involving pull requests:

  1. Workflow-level permissions block: This controls the scope of the GITHUB_TOKEN that is automatically generated for each workflow run. It dictates what the token is capable of doing.
  2. Repository-level GitHub Actions permissions: These are broader settings that control whether GitHub Actions as a feature is allowed to perform certain sensitive operations at all within that specific repository, regardless of the token's scope.

Our workflow token had the pull-requests: write scope, but the repository itself was configured to restrict GitHub Actions from creating PRs. This is a crucial security feature, but one that can certainly trip you up!

To fix this, we needed to update the repository's GitHub Actions settings. This isn't something you can do directly in your .github/workflows YAML. Instead, it requires using the GitHub API or navigating to your repository settings.

We used the gh cli (GitHub CLI) to update these settings:

bash
# First, ensure you're authenticated with `gh auth login`
# Replace {owner} and {repo} with your repository details

gh api \
  --method PUT \
  /repos/{owner}/{repo}/actions/permissions/workflow \
  -F default_workflow_permissions='write' \
  -F can_approve_pull_request_reviews=true

Let's break down those critical flags:

  • default_workflow_permissions='write': This sets the default permissions for newly created workflows to write (it was previously read). While our specific workflow had permissions explicitly set, it's good practice to align the default.
  • can_approve_pull_request_reviews=true: This was the key! This specific setting dictates whether GitHub Actions are allowed to create or approve pull requests within the repository. It's a global toggle for the Actions feature itself, independent of an individual workflow's token scope.

The Resolution: Green Lights All Around!

Once both layers of permissions were aligned—the workflow's token having write access for contents and pull-requests, and the repository allowing Actions to create PRs—our Vibe Publisher workflow sprang to life!

The workflow passed green (run 22483155060), successfully creating a new branch and opening a pull request from our .memory/ session letter. Victory!

This fix means that all four of our core CI/CD workflows are now fully operational: our linting, unit tests, end-to-end tests, and now the Vibe Publisher.

Beyond Permissions: A Productive Session

While tackling the GitHub Actions permissions was the main event, this session also saw significant progress on other fronts:

  • Enriched Project List: Our dashboard's project list page now boasts per-card stats, including workflows, discussions, notes, action points, spend, and success rate. This provides a much richer overview at a glance.
  • CI Pipeline Fortification: We fixed all three of our CI pipeline jobs, resolving issues with lint plugin registration, our Kimi test model, and ensuring the pgvector extension was correctly configured in our E2E environment.
  • Lint Cleanup: We diligently cleaned up approximately 40 pre-existing lint errors across about 25 files, making our codebase a little tidier and more consistent.

Looking Ahead

With our automation pipeline now robust, our immediate next steps include:

  1. Visually verifying the enriched project list page in the browser to ensure everything looks as intended.
  2. Considering adding a tooltip to the success rate badge, clarifying it's "of completed runs."
  3. Evaluating whether to denormalize stats into our Project model if the list query becomes slow at scale.
  4. And, in a wonderfully meta twist, reviewing the auto-generated blog post PR that the Vibe Publisher will create from this very session memory!

This journey through GitHub Actions permissions was a fantastic reminder that sometimes, the simplest-sounding tasks require the deepest dives into configuration layers. Understanding these nuances is crucial for building robust, secure, and automated development workflows. Happy automating!