Signing commits from bot accounts and automation scripts in Github Actions

 ⋅ 2 min read

It is very important to sign your Git commits.

Although it is quite easy to generate your own GPG key and use it to auto-sign all your Git commits, it is difficult to sign commits coming from automation scripts, bot accounts and CI steps. For example, for many implicit git commits on Github Actions, the default github-actions bot user account is used for (unsigned) commits.

I recently ran into a compliance requirement that needed every single commit in the main branch to be signed and verified. We use semantic-release to make releases and commit back the latest version to our package.json version. With the signing requirement, we had to remove semantic-release from our CI workflow, as it uses its own bot account for commits – which are unsigned.

However, I was determined to bring this step back while remaining compliant, so I did just that.

Step 1 – Set up your GPG keys

Follow Github's guide on adding a new GPG key to your account to first set up your keys.

Personally, I did not want to use my personal GPG keys to sign commits at work, so I created a dedicated bot account on Github. Once I did that, I generated new GPG keys for the bot account. I finally added the GPG keys to the bot's Github account as explained above. Make sure to securely note down the passphrase (you should use one and not leave it blank!) and the private & public keys. You'll need these for the next step.

Step 2 – Set up GPG passphrase & private key as secret

The GPG passphrase and the private key need to be set up as encrypted secrets in the Github repository of your choice. I named them BOT_GPG_PASSPHRASE and BOT_GPG_PRIVATE_KEY.

Step 3 – Configure commits to be signed with the GPG key

Adjust your Github Actions workflow to import the GPG key and use it to sign your commits.

name: Release

on:
  push:
    branches:
      - main

jobs:
  release:
    name: release
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
      with:
        persist-credentials: false # This is important if you have branch protection rules!
    - name: Import bot's GPG key for signing commits
      id: import-gpg
      uses: crazy-max/ghaction-import-gpg@v4
      with:
        gpg_private_key: ${{ secrets.BOT_GPG_PRIVATE_KEY }}
        passphrase: ${{ secrets.BOT_GPG_PASSPHRASE }}
        git_config_global: true
        git_user_signingkey: true
        git_commit_gpgsign: true
    - name: Change some files
      run: echo 'adding a new commit now' >> README.md
    - name: Commit changes to README.md file
      run: git commit -m "this is bot" README.md
      env:
        GITHUB_TOKEN: ${{ secrets.OSLASH_BOT_GITHUB_TOKEN }}
        GIT_AUTHOR_NAME: ${{ steps.import-gpg.outputs.name }}
        GIT_AUTHOR_EMAIL: ${{ steps.import-gpg.outputs.email }}
        GIT_COMMITTER_NAME: ${{ steps.import-gpg.outputs.name }}
        GIT_COMMITTER_EMAIL: ${{ steps.import-gpg.outputs.email }}

Here's a working example of such a workflow – getoslash/eslint-plugin-tap, and a commit that was made from an automated CI step.