Post

Push commits to another repository with GitHub Actions

Breaking up my website’s mono-repo had some unintended consequences. I wrote a script and Actions workflow that automatically updates my profile README whenever I published a new post. Since the website and the profile README are now in two separate repos, this whole job broke.

It’s fixed now, but found a few not-obvious things about how Actions works with multiple repositories to share.

  1. A workflow can only be triggered by an event happening inside the repository that hosts it1. This means that the job to update my profile README file must also be in the private repository where I’m committing the markdown files for new posts.
  2. GITHUB_TOKEN can only be scoped to the repo running the workflow. I mean, this is one of the most sensible security decisions ever. It’d be quite dangerous to allow something in a repository to do something somewhere else by default. The permission federation could get confusingly complicated too. It’s just ever so slightly annoying in this one case.
  3. The new “fine-grained” tokens are much more configurable than account-scoped credentials, allowing for simpler security and just as easy to set up as the old tokens.

To fix this problem in the short term, I created a new “fine-grained” personal token that can read and write to my profile repository only. It can only read and write to a single repository, not do any number of other tasks like manage billing or issues or who knows what. Having a tightly-scoped token stored for only this workflow to use is quick and easy to do.

actions/checkout is smarter than it gets credit for

There’s about a billion configuration options for actions/checkout and this was one of them. What I really want to do is check out two repositories side-by-side, but using different credentials for each one. The end goal should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── website/
│   ├── .github/
│   │   ├── scripts/
│   │   │   ├── requirements.txt
│   │   │   └── latest-posts.py        # Python script to edit profile/README.md
│   │   └── workflows/
│   │       └── update-readme.yml      # the workflow that runs the script
│   └── _posts/
│       ├── yyyy-mm-dd-titlehere.md    # posts that have content to be
│       └── yyyy-mm-dd-anotherpost.md  # gathered by the script for updates
└── profile/
    └── README.md                      # the file to update

The small Python script at ~/.github/scripts/latest-posts.py hasn’t changed much (link to the latest version). However, the GitHub Actions workflow changed to check out two repositories side-by-side, using content from one to update content in another. Turns out that was really straightforward to do.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
jobs:
  update-posts:
    runs-on: ubuntu-latest
    steps:

      - name: Checkout website repo
        uses: actions/checkout@v4
        with:
          path: website

      - name: Checkout profile repo
        uses: actions/checkout@v4
        with:
          repository: some-natalie/some-natalie
          path: profile
          token: ${{ secrets.WEBSITE_UPDATE }}

      - name: Get latest 3 posts and descriptions, insert into README.md
        shell: bash
        run: |
          pip3 install -r ./website/.github/scripts/requirements.txt
          python3 ./website/.github/scripts/latest-posts.py

      - name: Commit and push changes (if any)
        shell: bash
        env:
          CI_COMMIT_MESSAGE: update profile readme with latest posts
          CI_COMMIT_AUTHOR: github-actions[bot]
          CI_COMMIT_EMAIL: username@users.noreply.github.com
        run: |
          cd profile
          git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}"
          git config --global user.email "${{ env.CI_COMMIT_EMAIL }}"
          if [[ `git status --porcelain --untracked-files=no` ]]; then
            # Changes
            git add README.md
            git commit -m "${{ env.CI_COMMIT_MESSAGE }}"
            git push
          else
            # No changes
            echo "no changes to latest posts"
            exit 0
          fi

This works great for today, but next up is to remove the need to manage personal access tokens entirely. I will 100% forget about the name or scope of this by the time it expires in 3 months, but why should any credential live that long to begin with?


  1. This isn’t exactly true. There’s a webhook system to trigger workflow runs in Actions called repository_dispatch that I won’t be bothering with because it seems a bit much for such a simple task. It feels equally kludgey to do this versus what I’ve put together. 

This post is licensed under CC BY 4.0 by the author.