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.
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):
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:
- Workflow-level
permissionsblock: This controls the scope of theGITHUB_TOKENthat is automatically generated for each workflow run. It dictates what the token is capable of doing. - 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:
# 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 towrite(it was previouslyread). While our specific workflow hadpermissionsexplicitly 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
pgvectorextension 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:
- Visually verifying the enriched project list page in the browser to ensure everything looks as intended.
- Considering adding a tooltip to the success rate badge, clarifying it's "of completed runs."
- Evaluating whether to denormalize stats into our Project model if the list query becomes slow at scale.
- 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!