DEV Community

Cover image for Create a Pull request into another repository using github actions.
Dimitrios Desyllas
Dimitrios Desyllas

Posted on

Create a Pull request into another repository using github actions.

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:

  1. Upon mkdotenv actions create the homebrew tap
  2. Clone homebrew repo
  3. Create a branch and copy the file into Formula folder
  4. Test that app is installed
  5. push the branch
  6. 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:

  1. 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 repo

  2. Local 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:

  1. pc-magas/.github-access-token in which I have the general policies of allowed action upon access-token.yaml in it
  2. pc-magas/homebrew-mkdotenv The homebrew tap in which pr's would be autogenerated
  3. pc-magas/mkdotenv in which:
    1. Formula file is created
    2. Github action create and push a branch upon pc-magas/homebrew-mkdotenv with the new formula
    3. 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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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\"}"


Enter fullscreen mode Exit fullscreen mode

The flow follows a simple philosophy:

  1. Clone the target repo
  2. 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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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\"}"
Enter fullscreen mode Exit fullscreen mode

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)