Upon developing the mkdotenv project I hgad this partucilar problem. I needed to release upon mac as well and for that I need a repo that hosts the homebrew tap.
The procedure I developed is the following:
- Upon mkdotenv actions create the homebrew tap
- Clone homebrew repo
- Create a branch and copy the file into
Formula
folder - Test that app is installed
- push the branch
- Create the PR
The last steps was the one that caused me pain therefore I would explain what I did to solve it.
Required apps and settings I needed to install
Step1 Install Qoomon Access Tokens for GitHub Actions
I used the qoomon/actions--access-token project, that provided me a GitHub App that can issue temporary tokens during a workflow run. These tokens are powerful because they can grant fine-grained permissions such as pushing branches or opening PRs across repositories — something the default GITHUB_TOKEN cannot always do. Also it is a better alternative than storing PAT upon secrets as well.
In my case I installed it from Marketplace. Setup is free, but since it’s an app install, but I needed to go through the standard GitHub "purchase" flow (at $0), that required meto provide some basic private info (name and address mostly).
Once the app is installed, I had to define token policies using YAML files. There are two levels of configuration:
Global policies (.github-access-token repo)
You create a dedicated repository named .github-access-token.
Inside it, you add an access-token.yaml file that defines the general rules: which repositories can request tokens, and what operations are allowed.
Think of this as your organization-wide or user-wide "master policy".
👉 Example: my .github-access-token repoLocal policies (.github/access-token.yaml in a repo)
Inside each target repo (the one you want to push to or create a PR against), you also create a file at .github/access-token.yaml.
This file must match the owner of the global repo above and acts as a per-repository confirmation.
It narrows down the rules from the global file to the specific repository, making sure you explicitly allow those actions in that repo.
In my case I have 3 repos:
-
pc-magas/.github-access-token
in which I have the general policies of allowed action uponaccess-token.yaml
in it -
pc-magas/homebrew-mkdotenv
The homebrew tap in which pr's would be autogenerated -
pc-magas/mkdotenv
in which:- Formula file is created
- Github action create and push a branch upon
pc-magas/homebrew-mkdotenv
with the new formula - A pr is created
As you notice all of them are owned by pc-magas
.
The general flow goes as follows:
+---------------------------+ +-----------------------------+
| pc-magas/mkdotenv | | pc-magas/homebrew-mkdotenv |
| (creates formula) | | (receives PRs) |
| | | |
| GitHub Action runs ---> | ----push----> | Branch created |
| | | PR opened |
+---------------------------+ +-----------------------------+
| ^
| |
v |
+---------------------------+ |
| .github-access-token repo | |
| (global policy) | |
| access-token.yaml | |
+---------------------------+ |
| |
| defines who can request tokens |
v |
+---------------------------+ |
| target repo config | |
| .github/access-token.yaml |-----------------------------+
| (local policy) |
+---------------------------+
|
v
Tokens issued at workflow runtime
with correct permissions
Step 2 configure generic policy upon .github-access-token
repo.
As described above you need to create a repo named .github-access-token
, on it I needed to create a file named access-token.yaml
.
In my case these are the settings I needed to place upon:
origin: pc-magas/.github-access-token
allowed-repository-permissions:
actions: write # read or write
contents: write # read or write
pull-requests: write # read or write
# Grant owner scoped permissions (owner permission or owner wide repository permissions)
# NOTE: Every statement will always implicitly grant `metadata: read` permission.
statements:
- subjects:
- repo:${origin}:ref:refs/heads/dev
- repo:${origin}:workflow_ref:${origin}/.github/workflows/release.yml@refs/heads/dev
permissions:
pull-requests: write
1. origin
The origin field tells the app which .github-access-token repo defines the global policy.
It always follows the format:
OWNER/.github-access-token
-
OWNER
= your GitHub username or organization name -
.github-access-token
= the special repository that stores the global rules
Examples:
For my personal account (pc-magas), the repo is named
pc-magas/.github-access-token
If the owner was an organization named ellakcy, then it would be
ellakcy/.github-access-token
This matters because when a workflow requests a token, the app checks:
- Which global policy repo (origin) it should look at
- Whether that policy allows issuing the token for the requested action
2. allowed-repository-permissions
This section sets the maximum permissions a token can have when issued.
Think of it as the "permission budget".
For example:
-
actions: write
→ allows running workflows -
contents: write
→ allows pushing/pulling commits -
pull-requests: write
→ allows creating or updating PRs
There is an full template that can be used upon for reference.
3. statements
This section defines who is allowed to request a token and what extra permissions they get.
It works like an access control rule:
-
subjects
: → specifies which workflows or branches can ask for tokens -
permissions
: → specifies what those subjects are allowed to do
Example from above:
statements:
- subjects:
- repo:${origin}:ref:refs/heads/dev
- repo:${origin}:workflow_ref:${origin}/.github/workflows/release.yml@refs/heads/dev
permissions:
pull-requests: write
This means:
- Only workflows running from the dev branch of the .github-access-token repo can request a token.
- Those workflows are granted pull-requests: write permission (so they can open or update PRs).
If you want to allow multiple repositories you need to add multiple sections and what permissions are allowed.
Step 3 Define a local policy on repo you want to perform PR
The local policy lives inside the target repository — the repo where you want to push code or create a pull request.
This policy acts as an explicit opt-in: it tells GitHub, "yes, I allow workflows from these repos/branches to perform actions here."
For my case, this file lives in pc-magas/homebrew-mkdotenv
repo upon.github/access-token.yaml
file:
origin: pc-magas/homebrew-mkdotenv
statements:
- subjects:
- repo:pc-magas/mkdotenv:** # all refs, workflows, environments
permissions:
contents: write
pull-requests: write
The first thing you notice is that this file is a more slimmed-down version of the global policy. Only origin and statements are present here. As before, the origin field simply points to the repository where this local policy file lives. Then, under the statements section, you describe which repositories are allowed to interact with this one and what they are allowed to do.
You can add one or more entries under statements. Each entry can target a different repository or workflow and specify the actions it may perform. The actual actions are defined in the permissions section.
In my case, I allow anything from pc-magas/mkdotenv — whether it’s a branch, a workflow, or an environment — to push code and open pull requests into pc-magas/homebrew-mkdotenv
.
Implement Github action
A minimal example of my full worflow is shown below.
Upon myrepo the pipeline has multiple jobs, but the one that matters for creating the pull request is test_homebrew
.
test_homebrew:
runs-on: macos-latest
permissions:
contents: write
id-token: write
needs:
- release
- build_mac
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download homebrew formula for macos
uses: actions/download-artifact@v4
with:
name: mkdotenv-macos-homebrew-formula
path: ./macos/bin
- name: Generate GitHub App token
id: token
uses: qoomon/actions--access-token@v3
with:
repository: pc-magas/homebrew-mkdotenv
permissions: |
contents: write
pull_requests: write
- name: setup git and clone brew formula
env:
GH_PAT: ${{ steps.token.outputs.token }}
run: |
git config --global user.name "github-actions"
git config --global user.email "actions@github.com"
git clone https://x-access-token:${GH_PAT}@github.com/pc-magas/homebrew-mkdotenv.git
cd homebrew-mkdotenv
git checkout -b test-update-formula-${{ github.run_number }}
- name: setup formula
run: |
cd homebrew-mkdotenv
mkdir -p ./Formula
cp ../macos/bin/mkdotenv.rb ./Formula/mkdotenv.rb
- name: Create Pull Request to homebrew repo
env:
GH_PAT: ${{ steps.token.outputs.token }}
run: |
cd homebrew-mkdotenv
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
git add ./Formula/mkdotenv.rb
git commit -m "Update formula via GitHub Actions"
git push origin ${BRANCH_NAME}
VERSION=$(grep -E '^ version ' ./Formula/mkdotenv.rb | sed 's/.*"\(.*\)".*/\1/')
curl --fail-with-body -vvv -X POST https://api.github.com/repos/pc-magas/homebrew-mkdotenv/pulls \
-H "Authorization: Bearer $GH_PAT" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "{\"title\": \"Update formula [VERSION $VERSION]\",\"head\": \"$BRANCH_NAME\",\"base\": \"master\",\"body\": \"Automated update\"}"
The flow follows a simple philosophy:
- Clone the target repo
- Create a new branch 3.Copy or modify the necessary files 4.Commit and push the changes 5.Open a pull request
The key part is how authentication works. The GitHub App token is generated here:
- name: Generate GitHub App token
id: token
uses: qoomon/actions--access-token@v3
with:
repository: pc-magas/homebrew-mkdotenv
permissions: |
contents: write
pull_requests: write
This uses the qoomon/actions--access-token
action to issue a temporary token with just the right permissions. The token is then exposed as teps.token.outputs.token
and passed into later steps through an environment variable called GH_PAT
like this:
- name: setup git and clone brew formula
env:
GH_PAT: ${{ steps.token.outputs.token }}
run: |
# These are needed in order to config which does the push and PR
git config --global user.name "github-actions"
git config --global user.email "actions@github.com"
git clone https://x-access-token:${GH_PAT}@github.com/pc-magas/homebrew-mkdotenv.git
cd homebrew-mkdotenv
# Create new branch for PR
git checkout -b test-update-formula-${{ github.run_number }}
When cloning the repo, authentication is set up by injecting that token into the HTTPS URL:
git clone https://x-access-token:${GH_PAT}@github.com/pc-magas/homebrew-mkdotenv.git
Notice the x-access-token:${GH_PAT} part inside the repository URL. This is what enables Git to authenticate in a non-interactive way during the workflow, so the branch can be created and pushed automatically.
The PR itself is done using the Github rest api, we use the very same key we used upon cloning.
For api consumption we need to use curl
, the call is performed like this:
curl --fail-with-body -X POST https://api.github.com/repos/pc-magas/homebrew-mkdotenv/pulls \
-H "Authorization: Bearer $GH_PAT" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "{\"title\": \"Update formula [VERSION $VERSION]\",\"head\": \"$BRANCH_NAME\",\"base\": \"master\",\"body\": \"Automated update\"}"
The generated key is provided as bearer token. Also when using curl
upon github actions it is nice to aplo place the --fail-with-body
argument, that allows if rest api returns an error http status code (such as 4XX or 5XX) the whole step fails, that allows you to have a better view whether PR was created or not.
Top comments (0)